class X { };
class Y : public virtual X { };
class Z : public virtual X { };
class A : public Y, public Z { };
它们的sizeof结果如下:
sizeof(X) = 1;
sizeof(Y) = 8; //视编译器不同而不同
sizeof(Z) = 8; //视编译器不同而不同
sizeof(A) = 12;
实际上,class X 并不是空,它有一个晦涩的1bytes,那是被编译器安插进去的一个char, 这使得这个class的两个对象将在内存中有不同的地址。
而class Y 和 class Z 与三个因素有关。
一:语言本身造成的额外负担。当语言支持virtual base class 时,会导致一些额外的负担。在派生类中,这个额外负担反映在指针身上,它或者是指向virtual base class subobject;或者指向一个表格,表格中存放的若不是virtual base class subobject的地址,就是其偏移量。
二:编译器对于特殊情况所提供的优化处理。Virtual base class X 的subobject的1bytes大小也出现在class Y 和 Z 身上。传统上它被放在派生类固定部分的尾端。而某些编译器会对empty virtual base class 提供特殊支持。
三:Alignment 的限制。Class Y 和 Z 目前为5字节,为了进行对齐,将会被变成8字节。
而在某些编译器中,比如visual C++中,结果为1,4,4,8。这是因为编译器提供了特殊处理。它的模型如下:
对于Class A,它的大小是多少呢?由以下决定(一个虚拟继承的基类只有一个实体,不管被继承了多少份)
一:被大家共享的唯一一个class X 实体,1个字节。
二:Base Y 的大小,为4字节。Z也一样。
三:Class A 自己的大小0字节。
四:alignment的情况。这个时候为9字节,为了对齐,所以为12字节。
而visual C++进行特殊处理后,Base Y + Base Z 的大小为8字节。
每一个静态数据都只有一个实体,存放在程序的data segment中,每次程序取用static member,就会被内部转化为对该唯一的extern实体的直接参考操作:
origin. chunksize = 250 等价于 Point3d::chunksize = 250。
pt->chunksize = 250 等价于 Point3d::chunksize = 250。
在C++中,这是“通过一个指针和通过一个对象来存取数据成员时,结论完全相同”的唯一一种情况。因为此时的member并不在对象中。
Point3d origin, *pt = &origin;
origin.x = 0.0
pt->x = 0.0
"从origin存取“和”从pt存取'有什么重大的差异吗? 答案是“当Point3d是个派生类,而x数据成员是基类的数据成员时,就有重大的差异。对于指针访问数据而言,这个操作必须延迟到执行时期进行访问。而如果使用origin,则在编译时期就确定了。
class Concrete
{
private:
int val;
char c1;
char c2;
char c3;
};
sizeof(Concrete) = sizeof(val) + sizeof(char)*3 + alignment = 8;
class Concrete1
{
private:
int val;
char c1;
};
class Concrete2:public Concrete1
{
private:
char c2;
};
class Concrete3: public Concrete2
{
private:
char c3;
};
这个时候,sizeof(Concrete3)并不等于8。
sizeof(Concrete3) = sizeof(Concrete1) + sizeof(Concrete2) + sizeof(Concrete3) = 8 + 4 + 4 = 16。
因为虚拟继承只有一个基类,如何对虚拟继承的基类进行布局呢?一种方法是把派生类分成两个部分:一个不变局部和一个共享局部。
一般的布局策略是先安排好derived class 的不变部分,然后再建立其共享部分。那么如何存取class的共享部分呢?cfront 编译器会在每一个派生类对象中安插一些指针,每个指针指向一个virtual base class。这有两个问题:
一:每一个对象必须针对其每一个虚基类背负一个额外的指针。
二:由于虚拟继承串链的加长,导致间接存取层次的增加。这里的意思是:如果我有三层虚拟,我就需要三次间接存取。
MetWare和其它编译器对于第二个问题的解决方法是:复制嵌套的虚拟类的指针放到派生类对象中,虽然付出了一些空间上的代价,但是访问时间不会随着继承的增加而增加时间。
至于第一个问题,一般而言有两个解决方法。Microsoft编译器引入所谓的virtual base class table。virtual base class指针放在这个表格中。第二个解决方法是:是在virtual function table中放置virtual base class的offset。
class Point3d
{
public:
virtual ~Point3d();
static Point3d origin;
float x, y, z;
};
那么& Point3d::z; 得到的是什么? 将得到z 在类对象中的偏移量。如果vptr放在对象的起头,则三个坐标值在对象布局中的offset分别是4, 8 , 12。然而我们若去取data members的地址时, 传回的值应该是多1的, 也就是1, 5, 9。因此:
printf("%p/n", &Point3d::x);
printf("%p/n", &Point3d::y);
printf("%p/n", &Point3d::z);
在VC6.0中编译得到的结果是:
00000004
00000008
0000000C
在BCB3中编译得到的结果是:
00000005
00000009
0000000D
这说明vptr放在编译器的起头。VC6.0得到的结果可能进行了特殊处理。好!现在回到正题,为什么值要加1呢?看下面这个例子。
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
if( p1 == p2 )
{
cout << "p1 & p2 contain the same value --";
cout << " they must address the same member!" << endl;
}
为了区分p1和p2, 每一个真正的member offset值都被加上1。 因此,不论编译器或使用者都必须记住,在真正使用该值以指出一个member之前,请先减掉1。
为了测试VC6.0的值为什么没有加1,我测试程序如下:
class Point3d
{
public:
static Point3d origin;
float x, y, z;
};
int main()
{
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
if( p1 == p2 )
{
cout << "p1 & p2 contain the same value --";
cout << " they must address the same member!" << endl;
}
return 0;
}
发现p1的值为0xffffffff, p2的值为0x00000000。不执行输出。
if( p1 == NULL )
{
cout << "p1 & p2 contain the same value --";
cout << " they must address the same member!" << endl;
}
p1的值为0xffffffff, 执行输出。
但是NULL的值为0啊!为什么会执行输出呢?
我的猜测是VC6.0对指针的值还是加1了,打印出来的结果是减1后的。
因此:
& Point3d::z; 和 & origin.z之间的差异,就非常明确了。前者取”它在类中的偏移量,而后者取类对象中z的真正的地址“。