我用vc2003观测到的实际情况是。在类中增加一个指针(VBPTR)指向一个VBTBL,这个VBTBL的第一项记载的是从VBPTR 与本类的偏移地址,如果本类有虚函数,那么第一项是FF FF FF FC(也就是-4),如果没有则是零,第二项起是VBPTR与本类的虚基类的偏移值。vc2003的这种方案个人觉得没有Bjarne的好,一是要多一个指针,二是因为VBPTR与虚函数表分开设计,也不便于修改。至于其它编译器,因为我跟其它编译器不熟,所以也就没有实测它们。
下面给出对于类定义
struct B1
{
int a;
int b;
};
struct B2
{
virtual void foo(void);
int c;
int d;
};
struct Test : virtual public B1, virtual public B2
{
virtual void func1(void);
virtual void func2(void);
virtual void func3(void);
int X;
};
一个Test 对象的内存布局图,我们可以清楚的看到在VS2003中VBPTR以及VBTBL的结构以及其相关的内容是什么意义。以及Bjarne的方案的优点。
最后我们来看一个完整的例子以及内存结构布局。图后有相关代码。
代码如下:
struct A
{
A(int v=100):X(v){};
virtual void foo(void){}
int X;
};
struct B :virtualpublic A
{
B(int v=10):Y(v),A(100){};
virtual void fooB(void){}
int Y;
};
struct C : virtual public A
{
C(int v=20):Z(v),A(100){}
virtual void fooC(void){}
int Z;
};
struct D : public B, public C
{
D(int v =40):B(10),C(20),A(100),L(v){}
virtual void fooD(void){}
int L;
};
int _tmain(int argc, _TCHAR* argv[])
{
A a;
int *ptr;
ptr = (int*)&a;
cout << ptr << " sizeof = " << sizeof(a) <<endl;
for(int i=0;i<sizeof(A)/sizeof(int);i++)
{
if(ptr[i] < 10000)
{
cout << dec << ptr[i]<<endl;
}
else cout << hex << ptr[i] <<" = " << hex << * ((int*)(ptr[i])) <<endl;
}
cout << "--------------------------------------" <<endl;
B b;
ptr = (int*)&b;
cout <<"addr:" << ptr << " sizeof = " << sizeof(b) <<endl;
for(int i=0;i<sizeof(B)/sizeof(int);i++)
{
if(ptr[i] < 10000)
{
cout << dec << ptr[i]<<endl;
}
else cout << hex << ptr[i] <<" = " << hex << * ((int*)(ptr[i])) <<endl;
}
cout << "--------------------------------------" <<endl;
D d;
ptr = (int*)&d;
cout <<"addr:" << ptr << " sizeof = " << sizeof(d) <<endl;
for(int i=0;i<sizeof(D)/sizeof(int);i++)
{
if(ptr[i] < 10000)
{
cout << dec << ptr[i]<<endl;
}
else cout << hex << ptr[i] <<" = " << hex << * ((int*)(ptr[i])) <<endl;
}
return 0;
}
另外看的一篇文章也很有启发性,大致内存结构也与上同
单个的虚继承没有意义,而且它的布局和普通的单继承完全一样。
虚继承主要用于多继承中的菱形继承,举例如下:
- class Top
- {
- public:
- int a;
- };
-
- class Left : virtual public Top
- {
- public:
- int b;
- };
-
- class Right : virtual public Top
- {
- public:
- int c;
- };
-
- class Bottom : public Left, public Right
- {
- public:
- int d;
- };
它的继承关系图如下:
可以得出 Bottom 的内存结构可能是这样子的。
从图中可以看出,Top 中的数据只保存了一份,也就是说,虚拟继承其实就是共享数据的意思。
但是虚继承的内存结构实际上没有这么简单,它应该是这样子的:
上图有两点值得大家注意。第一点就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。第二点,类中增加了vptr指针,这些是被编译器在编译过程中插入到类中的(在设计类时如果使用了虚继承,虚函数都会产生相关vptr)。同时,在类的构造函数中会对相关指针做初始化,这些也是编译器完成的工作。Vptr指针指向了一个“virtual table”。在类中每个虚基类都会存在与之对应的一个vptr指针。
我对虚继承、普通继承中虚函数的内存放置总结成几句话:
1、虚继承时子与基需分开。子在前,基在后。(子类的虚函数与积累的虚函数需要分开,不可覆盖)
2、普通继承时,基在前,子在后。如果是多重继承,则顺序是基1-基2-最子类-最基类(子类的虚函数如果和基类相同,会覆盖子类的虚函数)