多重虚继承下的对象内存布局
《深入C++对象模型》绝对是一本值得深读的一本书,书里多次出现一句话,“一切常规遇见虚继承,都将失效”。这是一个有趣的问题,因为C++标准容忍对象布局的实现有较大的自由,出现了各编译器厂商实现的方式不同。
今天谈谈visual studio2013多重虚继承下对象布局。有错不要客气,不要吝啬你的留言。
class y和class z都是从class x虚继承来的子类(也叫派生类),class A是class y和class z的多重继承子类。为了简化问题,下面的data member都是none_static data member。不提static data member是为了描述起来简单~
#include<iostream> class x { public: int _x; }; class y : public virtual x { public: int _y; }; class z : public virtual x { public: int _z; }; class A:public z,public y { public: int _a; }; int main() { std::cout <<"sizeof(x): "<< sizeof(x) << std::endl; std::cout <<"sizeof(y): "<< sizeof(y) << std::endl; std::cout <<"sizeof(A): "<< sizeof(A) << std::endl; std::cout <<"&A::_x :"<< &A::_x << std::endl;; std::cout << "&A::_y :" << &A::_y << " " << std::endl; std::cout << "&A::_z :" << &A::_z << " " << std::endl; std::cout <<"&A::_a :"<< &A::_a << " " << std::endl; getchar(); return 0; }
输出:
sizeof(x): 4
sizeof(y): 12
sizeof(A): 24
sizeof(x)和sizeof(y)的结果在我们的意料中:x中没有虚函数,所以sizeof(x) = sizeof(int)。class y虚继承自class x,它需要一个指针指向类似于virtual base table的指针(下面简称vbptr),指向自己实际的x对象地址,所以sizeof(y)=2*sizeof(int) + sizeof(vbptr),我使用32位编译器生成的代码,所以sizeof(y)=12.
按照我开始的料想,sizeof(A)应该等于sizeof(int)*4(分别是:_x,_y,_z,_a) + sizeof(vbptr)=20。这里的vbptr指向了唯一的x实例。
(注意啦,我要开始了)
C++标准中有规定,子类的对象模型中,应该保持父类对象的布局。在上面,我们说了sizeof(y)=2*sizeof(int) + sizeof(vbptr)。但,我们还说了,常规遇见虚继承就可能失效。
根据虚继承的概念,class x的data member只会出现class A中一次,所以sizeof(A)=sizeof(y::vbptr+y::_y + z::vbptr + z::_z + x::_x)
用公式表示出来sizeof(A) = sizeof(base class a1) +sizeof(base class a2) +...+ sizeof(base class an) + sizeof(virtual inheritance(虚继承父类)) - N*sizeof(virtual inheritance(虚继承父类))
所以A的对象大小等于:继承部分大小再加上A中自己data member 大小。
要非常注意的是,这里的大小是指数据对齐后的大小,这里的对齐需要注意两点:第一是,类对象中包含类对象时的对齐;第二是,父类对象的内存布局应该被保持:从普通继承(相对虚继承)的类中拆开公有的类对象,他们分别会保持内存布局。(非常的绕口,我决定把这个问题另开一篇帖子讨论,此处稍后放上链接)
而vbptr因为出在了两个父类中,已经不需要额外的一个vbptr指向了。
你是不是要问这个vbptr(虚基类表指针)到底是干什么的?
我们知道虚继承的父类x,无论它被派生多次,它在最后多重继承的子类中,只存在一份实例。这样可能出现这样的情况:
y object_y;
A object_A;
object_A._x =1;
object_y = object_A;//这里发生类对象的剪切,将子类中的非父类成员剪去,剩下的赋给父类。
可能你还没有明白,没关系,我再啰嗦一句~
抱歉没有很好的绘图工具,我们使用“|“来隔开内存中的成员。
object_y 的内存布局应该是这样的:vbptr | _y | _x,注意先实例化父类,再实例化子类的原则因为虚继承的原因失效了!这里实例化虚继承的子类对象时,先实例化非公有部分(也就是除了class x的data member部分:class y的int _y),再实例化,虚继承的父类的data member部分,也就是公有部分class x 中的int _x。
而,object_A的内存布局应该是这样的:vbptr_z | _z | vbptr_y | _y | _a | _x,注意我们多重继承的先后顺序:class A:public z,public y,这个顺序决定了z的对象布局在y对象前面。因为虚继承的原因,我们对象布局发生了改变:先实例化不共享部分(也就是class y和class z中除了class x的成员),这个部分依然是先父类再子类。最后才是虚继承的公共部分:class x的成员。
我们发现,直接进行内存布局的剪切行不通(红色部分),这需要编译器介入,但编译器也不是神仙啊,它需要信息去找_x在哪里,这就是vbptr中存的信息——它告诉编译器去哪里找_x,这里的信息可能是偏移,也可能是指针。为什么是表呢?而不是直接指向成员的指针呢?因为,如果你再虚继承几个父类,你的这个指针会越来越多,对象的规模也就越来越大,这不能容忍。使用表可以始终只占一个指针大小,只是需要间址查询,但是这完全可以由编译器优化之。