一、不包含继承、虚函数、虚继承、多重继承的类
此种简单class的对象内存分布由《C++中struct/class的数据对齐与sizeof》一文阐述,此处不再赘述。
二、单一继承、不含虚函数的类(非虚继承)
此类中,将把基类成员分布于对象低地址空间,在基类成员之后紧跟排列子类成员,且由于内存对齐,要满足以下两点条件:
1、C++语言保证“出现在derived class中的base class suboject有其完整性”;
2、derived class的对齐数 = min(指定的全局对齐数,max(base class的对齐数,derived class的对齐数))。
三、单一继承、含有虚函数的类(非虚继承)
此类中,除了要满足第二点中所提到的条件之外,由于引入虚函数,故类中有虚函数表,且对于单一继承,一个对象中只有一个虚表(子类的非覆盖虚函数的地址添加至基类虚表后方),所以在类对象中需要增加一个虚函数表指针指向该虚表,该虚表指针一般存储于对象所在内存的第一个字节中,虚表中函数的覆盖规则请参考《C++虚函数表解析》一文。
四、多重继承、基类与派生类均不含虚函数(非虚继承)
不含虚函数的多重继承是不含虚函数单一继承情况的推广。
首先,不含虚函数的多重继承中的多个基类将按照它们的声明顺序,在对象中依次排列,且仍然满足两条基本条件,即:
1、C++语言保证“出现在derived class中的每个base class suboject有其完整性”;
2、derived class的对齐数 = min(指定的全局对齐数,max(base class的对齐数,derived class的对齐数))。
但是在多重继承的情况下,派生类指针向基类指针的转换将会出现一个较为怪异的行为。
一般情况下,C++中对指针进行类型转换不会改变指针本身所指的内存位置,只会改变指针的类型。
在单一继承时,将基类成员排列在派生类成员之前,也是为了派生类指针转换为基类指针后,在调用时各个成员的偏移量与真正的基类对象相同。
但在多重继承情况下,将一个派生类的指针转换成某一个基类指针,编译器会将指针的值偏移到指向该基类在对象内存中的起始位置。若是待转换基类在对象中恰好排在第一个,则基类指针与派生类指针值相同,但是若待转换基类排列在第一个基类以后,转换后的基类指针将在数值上与派生类指针不同。但还有另外一现象,在以下代码中:
#include <iostream>
using namespace std;
class CBaseA
{
public:
char m_A[32];
};
class CBaseB
{
public:
char m_B[64];
};
class CDerive : public CBaseA, public CBaseB
{
public:
char m_D[128];
};
int main()
{
auto pD = new CDerive;
auto pA = (CBaseA *)pD;
auto pB = (CBaseB *)pD;
cout << pA << '\n' << pB << '\n' << pD << endl;
cout << (pD == pB) << endl;
}
"cout << (pD == pB) << endl"的输出结果将为真,这是因为: 当编译器发现一个指向派生类的指针和指向其某个基类的指针进行==运算时,会自动将指针做隐式类型提升已屏蔽多重继承带来的指针差异。因为两个指针做比较,目的通常是判断两个指针是否指向了同一个内存对象实例,在上面的场景中,pD和pB虽然指针值不等,但是他们确确实实都指向了同一个内存对象(即new CDerive;产生的内存对象 ),所以编译器又在此处插了一脚,让我们可以安享==运算的上层语义。
五、多重继承、基类或派生类中包含虚函数(非虚继承)
当多重继承中所继承的某个基类包含虚函数时,由于虚表存在,该基类对象将在内存起始处增加一个虚表指针,当有多个基类都包含虚表指针时,由于“派生类对象必须保证包含完整基类子对象”原则,在派生类对象中将包含有多个虚表指针,其内存分布情况如下图所示,每个虚表指针排布在它所属基类子对象的起始地址处。
多重继承中的虚函数覆盖规则请参考请参考《C++虚函数表解析》一文。 注意此处有以下两个原则:
1、含有虚表指针的基类总是排布在不含虚表指针基类之前,不管其声明顺序如何。
2、在派生类中定义的非覆盖虚函数地址,总是在第一个虚函数表后添加。
六、单一虚继承、不包含虚函数
当子类虚继承基类时,若不存在虚函数,在对象中添加一个虚基类表指针,该指针指向虚基类表,虚基类表中记录了从子类对象起始地址到虚基类子对象起始地址的字节偏移量。不同于非虚继承,虚继承中的基类子对象总是排在派生类成员的后方。
七、多重虚继承、不包含虚函数
多重虚继承中,不管虚基类含有多少个,在派生类对象中只存在一个虚基类表指针,该指针所指向的虚基类表依次记录了从派生类起始地址到每个虚基类子对象的偏移量。每个虚基类都排布在派生类成员之后,虚基类的排列顺序与它们的声明顺序相同。
八、既含有虚继承,又含有非虚继承的多重继承、不包含虚函数
在此种情况下,依旧只含有一个虚基类表指针,但要注意的是普通基类按情况四所述的情形排列在派生类成员的前部,而虚基类按情况六所述情形排列在派生类成员之后。
九、第一种钻石型继承
仔细分析钻石型继承,它实际由一个非虚多重继承与两个单一虚继承构成,故在ChildDerived对象中,Derived1和Derived2按其声明顺序排列在ChildDerived类成员变量之前,而每个Derived类子对象中包含一个虚继承表指针,它们的公共虚基类Base排列在ChildDerived对象内存的最后方,如下图所示:
十、第二种钻石型继承
这种钻石型继承与第一种相比,他的二级子类变成了从Derived1和Derived2的多重虚继承,如下图所示:
可将这种情况拆分为一个多重虚继承和两个单一虚继承的情况,在ChildDerived对象中有一个虚继承表指针,该虚继承表中包括了Base、Derived1和Derived2的偏移量。在ChildDerived类成员变量之后,以此排列Base类、Derived1类和Derived2类的子对象,且为了保持子对象的完整性,在Derived1类和Derived2类的子对象中也各含有一个虚继承表指针,指向存储有Base子对象偏移量的虚继承表。具体排列情况如下图所示:
十一、含有虚函数的虚继承
含有虚函数的虚继承在不同的编译器中有不同的实现方式,在VC编译器中,将虚函数表和虚继承表分开存储,故在派生类中需要两个指针分别指向两个表,而在GCC中,虚函数表和虚继承表合并为一个表,所以只需要一个指针即可访问它们。(参见文章http://www.cnblogs.com/cswuyg/archive/2010/08/20/1804113.html)
注:在虚继承中,派生类与基类将不再共享一个虚表指针,而是派生类含有一个虚表指针,基类含有另外一个虚表指针。
另,在此种情况下还有几点说明:
1、如果同时存在vfptr和vbptr,vfptr居前,vbptr居后;
2、普通基类居前,虚基类总是尽可能地排列在layout的最后;
3、两个同一层次的虚基类subobject,先声明者居前,后声明者居后,这点和普通基类是一样的;
4、两个不同层次的虚基类subobject,辈份高者居前,辈份低者居后。
参考文章:http://www.cnblogs.com/itZhy/archive/2012/10/08/2713367.html
http://www.cnblogs.com/cswuyg/archive/2010/08/20/1804113.html
http://blog.csdn.net/pathuang68/article/details/4101970 等(玄机逸士《对象内存布局》系列)