当我们写出以下代码并打印出它们sizeof结果:
class X{ };
class Y : public virtual X{ };
class Z : public virtual X{ };
class A : public Y, public Z{ };
X、Y、Z、A的继承关系如下图所示:
sizeof X、Y、Z、A 的结果为 1、8、8、12
在Visual C++ 5.0 上执行的结果为 1、4、4、8
我们下面来分析一下这几个类的大小的原因
X:它有一个隐藏的1 byte 大小,那是被编译器安插进去的一个char。这使得这一class的两个objects得以在内存中配置独一无二的地址:
X a, b;
if(&a == &b) cerr<<"yipes"<<endl;
Y和Z的大小受到三个因素的影响:
- 语言本身锁造成的额外负担:这个额外的负担反映在某种形式的指针身上,它或者指向virtual base class subobject,或者指向一个相关表格;表格中存放的若不是virtual base class subobject的地址,就是其偏移位置(offset)
- 编译器对于特殊情况所提供的优化处理
- Alignment的限制:(alignment就是将数值调整到某数的整数倍,32位就是4bytes)
某些编译器对此提供了特殊的处理,一个empty virtual base class 被视为derived class object最开头的一部分:
class A的大小必须视你使用的编译器而定
记住一个virtual base class subobject 只会在derived class 中存在一份实例,不管它在class继承体系中出现了多少次!
- 被大家共享的唯一一个class X实例,大小为1byte。
- Base class Y的大小,减去“因virtual base class X而配置”的大小,结果是4 bytes。Base class Z的算法亦同。加起来是8 bytes。
- class A自己的大小:0 byte。
- class A的 alignment 数量(如果有的话)。前述三项总和,表示调整前的大小是9 bytes。class A 必须调整至 4 bytes 边界,所以需要填补3 bytes。结果是12 bytes。
如果做了处理,class X实例的那1 byte将被拿掉,于是额外的 3 bytes 填补也不必了。
如果,我们在 virtual base class X 中放置一个(以上)的data members,两种编译器就会产生完全相同的对象布局。
总结:每一个class object 必须有足够的大小容纳它所有的nonstatic data members。有时候值回避想象中大:
- 由编译器自动加上额外的data members,用以支持某些语言特性(注要是各种virtual特性)。
- 因为 alignment(边界调整)的需要。
C++ Standard要求,在同一个access section(也就是private、public、protected等区段中),members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件即可。
虚拟继承将为“经由 base class subobject 存取class members” 导入一层新的间接性,比如:
origin.x = 0.0; //编译时期确定
pt->x = 0.0; //执行期,因为不确定pt必然指向哪一种 class type
把一个class分解为两层或更多层,有可能会为了“表现class体系之抽象化”而膨胀所需的空间。
例如把
class Concrete{
public:
//...
private:
int val;
char c1;
char c2;
char c3;
}
分裂为三层结构:
在32位的机器中,就会从8 bytes 变为 16 bytes。这是为了执行指针复制的操作时避免破坏父类的数据成员而做的数据膨胀。
目前在C++编译器有一个讨论的题目:
把vptr放置在class object的哪里会最好?在cfront编译器中,它被放在尾端,支持C语言的兼容性。到了C++ 2.0,开始支持虚拟继承以及抽象基类,并且由于面向对象范式的兴起,vptr就放在了起头处,代价就是丧失了C语言的兼容性。
至于 reference,则不需要针对可能的0值做防卫,因为 reference 不可能参考到“无物”(no object)。指向 data member 的指针,其 offset 的值总是被加上1,这样可以使编译系统区分出“一个指向 data member 的指针,用以指出 class 的第一个 member”和“一个指向 data member 的指针,没有指出任何 member”两种情况。
虚拟继承:
cfront编译器会在每一个derived class object中安插一些指针,每个指针指向一个 virtual base class。要取得继承得来的 virtual base class members,可以通过相关指针间接完成。
这样实现的模型有两个主要的缺点:
- 每一个对象必须针对其每一个 virtual base class 背负一个额外的指针。(可以在virtual function table 中放置 virtual base class 的 offset)。
- 由于虚拟继承串链的加长,导致间接存取层次的增加。
虚拟继承,使用 Virtual Table Offset Strategy 所产生的数据布局。