文章之前让我们来看看虚继承的背景:
如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最底层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一标识一个成员,那么共同基类在派生类中只有一份复制,这样不仅可以避免产生二义性,同时也节省内存。
举个课本上的例子:
继承层次模型中有一个公共基类Base(含有int型数据a),两个间接基类Base1(含有自身数据b1),Base2(含有自身数据b2),一个派生类Derived(含有自身数据d),在间接基类没有声明虚拟继承公共基类Base时,继承模型如下图所示:
如果将间接基类Base1和Base2都声明为虚拟继承Base,则有如下继承模型:
Derived类对象在内存中是如何布局的呢?在《深度探索c++对象模型》一书中给出了如下例子:
其内存布局模型为:
为了确保基类Point2d类在派生类Vertex3d中只有一份复制,Bjarne的解决方法是在virtual function table中放置virtual base class的offset(偏移量),由这个偏移量定位基类Point2d部分在其派生类(可以是Vertex3d,也可以是Point3d,或者Vertex)中的位置。为了验证这种确保基类在派生类中只有一份复制的方法,后续的测试都是在微软的vs2013上
进行,具体步骤如下:
第1步. 每个类中只含有简单的数据成员,左边是类的继承关系,右边是vs2013的内存布局模型:
通过右边的图我们可以得到如下信息:派生类D一共占有24个字节,在这24个字节的内存空间中,类B占有8个字节,类C占有8个字节,其公共基类A在最高地址占有4个字节。因为B和C都继承A,所以在B和C中都有一个virtual base class pointer(虚基类指针),这个指针指向另外的一个int型地址,这个地址存放的是公共基类的偏移量。如图,在B中vbptr指向的内存地址存放的是20(因为基类B的这部分在派生类D的起始偏移为0,A的偏移为20,所以A相对于B的偏移量为20),同理,A相对于C的偏移量为12(字节),所以C的vbptr指针指向的内存地址存放的是12.
第2步:将虚函数加入到虚继承的类中,如下图所示(左边为类继承关系,右边为内存分布图):
从图中可以看出:D一共占用36个字节的空间,-12代表虚函数f3在类D中的偏移为12字节(为什么要用负数暂时还没搞清楚),第一个-4代表在B部分的虚基类表指针在B部分的偏移(4个字节),第二个-4代表在C部分的虚基类表相对于C部分的偏移,-28代表虚函数f1的函数指针在D中的偏移。此外还可以看出,派生类D的虚函数f4被拼接到了f2的后面。上述测试的只是两种简单的情况,还有其他更复杂的情况(如class D:virtual public B,virtual public C、class D:virtual public B,public C)等等,在这些情况下,公共虚基类A的位置是变动的,根据具体情况来定。
为什么编译器要将类D的每一个数据成员以其自身特定的方式标识这么清楚?我想这一切都是为了精确定位this指针,例如:
int main(){
C cc;
A aa;
D dd;
dd.a = 3;
dd.b = 4;
dd.c = 5;
dd.d = 6;
cc = dd;
aa = dd;
return 0;
}
编译器如何知道将对象dd中含有类A部分的数据成员准确的赋值给类A的一个对象aa?这个时候就要借助上述每一个数据成员的位置偏移标记来调整this指针在对象dd中的位置,从而精确赋值。
以上是个人见解,欢迎讨论。