C++对象内存布局

想要研究对象的内存布局必须要去对应的内存去查看。

一、两种查看对象内存的方法

一种方法就是采用这篇博客中的方法,这个方法比较直观方便。http://www.cnblogs.com/jerry19880126/p/3616999.html

先选择我们写的C++源文件,右键选择属性,在弹出的对话框中选择左侧的C/C++->命令行,然后在其他选项这里写上/d1reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXXXXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。

另外一种就是采用指针:char (*pFun)(int); //定义一个返回类型为char,参数为int的函数

typedef可以让函数指针更直观方便typedef 返回类型(*新类型)(参数表)

typedef的功能是定义新的类型。typedef char (*PTRFUN)(int); 就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。后面就可以像使用int,char一样使用PTRFUN了。PTRFUN pFun;使用这个新类型定义了变量pFun

#include<iostream>   
#include<string>

using namespace std;
class Stack1{
public:
	virtual void test()
	{
		cout << "我是基类test"<< endl;
	}
private:
	int foundation;
};
class Stack2 :public Stack1
{
public:
	void test()
	{
		cout << "我是派生类test" << endl;
	}
private:
	int value;
};

int main()
{
	Stack1 mystack;
	Stack2 youstack;
	int * vfptrAdree = (int *)(&mystack);
	cout << "虚函数指针(vfptr)的地址是:\t" << vfptrAdree << endl;

	typedef void(*Fun)(void);
	Fun vfunc = (Fun)*((int *)*(int*)(&mystack));
	//我们把虚表指针的值取出来: *(int*)(&mystack),它是一个地址,虚函数表的地址
	//把虚函数表的地址强制转换成 int* : (int *) *(int*)(&mystack)
	//再把它转化成我们Fun指针类型 :(Fun)*(int *)*(int*)(&mystack)
	cout << "第一个虚函数的地址是:" << (int *)*(int*)(&mystack) << endl;
	cout << "通过地址,调用虚函数Base::test():";
	vfunc();

	system("pause");
	return 0;
}

二、对象模型概述

C++中,有两种数据成员(class data members):staticnonstatic,以及三种类成员函数(class member functions:staticnonstaticvirtual。

普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。注意对齐。除非为了实现虚函数和虚继承引入的隐藏成员变量外,C++类实例的大小完全取决于一个类及其基类的成员变量。

子类继承了父类的成员变量时,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。

有虚函数时,VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表。虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致。

编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类。注意虚表指针只有一个,子类并没有再生成虚表指针了。)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。

所以,如果是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*Base*的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态了。

有虚继承时:(这两个图及文字摘取的上面提到的博客中的)


未定义虚基类的时候,类DerivedDerivedClass,由外向内看,它并列地排布着继承而来的两个父类DerivedClass1DerivedClass2,还有自身的成员变量e(基类重复)


VC++虚继承下内存布局:

首先排列非虚继承的基类实例;

有虚基类时,为每个基类增加一个隐藏的VBPTR,除非已经从非虚继承的类那里继承了一个;

排列派生类的新数据成员;

在实例的最后,排列每个虚基类的一个实例。

采用虚基类的内存布局。基类被相当于共享了。但多了vbptr指针,其是各派生类对应的虚表指针

虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)。

下面总结一下(当基类有虚函数时):

1. 每个类都有虚指针和虚表;

2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;

3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

在构造和析构过程中,有时需要初始化一些隐藏的成员变量。最坏的情况下,一个构造函数要执行如下操作:

如果是“最终派生类”,初始化vbptr成员变量,调用虚基类的构造函数;

调用非虚基类的构造函数;

调用成员变量的构造函数;

初始化虚函数表成员变量;

执行构造函数体中,程序所定义的其他初始化代码。

另C++ Under The Hood这个文章讲解的也很不错,想更深入请看下。文章分析了各种情况下如何找到想要调用的函数,并分析了调用开销。

猜你喜欢

转载自blog.csdn.net/E_ROAD_BY_U/article/details/76026432