可以参考以下文章:
http://blog.csdn.net/haoel/archive/2007/12/18/1948051.aspx 陈皓 C++ 虚函数表解析
http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx 陈皓 C++ 对象的内存布局(上)
http://blog.csdn.net/haoel/archive/2008/10/15/3081385.aspx 陈皓 C++ 对象的内存布局(下)
http://www.cnblogs.com/cswuyg/archive/2010/08/20/1804113.html C++对象内存布局测试总结
这些文章的陈述和之前自己的理解还是比较吻合的。
但有一些细节需要注意一下,写在下面加深一下记忆。
(1)单一的一般继承 (带成员变量、虚函数、虚函数覆盖)
1)虚函数表在最前面的位置。
2)成员变量根据其继承和声明顺序依次放在后面。
3)在单一的继承中,被 overwrite 的虚函数在虚函数表中得到了更新 。
(2)多重继承 (带成员变量、虚函数、虚函数覆盖)
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的 f()函数都被 overwrite成了子类的 f() 。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
(3)重复多重继承 (带成员变量、虚函数、虚函数覆盖)
这个和上面一样,只不过最顶层的基类在二级子类里有两份同样的拷贝,在直接访问顶层基类的 成员变量时,会出现二义性。
d.ib = 0; //二义性错误
d.B1::ib = 1; //正确
d.B2::ib = 2; //正确
(4)单一的虚拟继承 (带成员变量、虚函数、虚函数覆盖)
(5)钻石型的虚拟多重继承 (带成员变量、虚函数、虚函数覆盖)
第4和第5种情况就比较复杂了,因为这个内存布局其实是和具体的编译器相关的,不同的编译器实现不一样。
我们可以参照教材《inside c++ object model》P121和P123中的介绍。分别是使用pointer strategy和virtual table offset strategy.
在单一虚拟继承时:
(1)VS编译器:无论有无虚函数,必然含有虚基类表指针。虚基类表中的内容为本类实例的偏移和基类实例的相对偏移值。如果有虚函数,那么基类的虚函数表跟派生类的虚函数表是分开的。
(2)GNU的GCC编译器:跟VS的编译器类似,有不同的地方是,虚基类表跟派生类的虚函数表合并。另外通过虚基类表指针往正负两个方向寻址,可以获得不同偏移值,也就是说有两个功能一样的虚函数表 。
最后总结一下虚基类表的问题:
VS编译器会去使用虚基类表,用于寻址虚基类地址(virtual base class table strategy )。而GCC编译器则没有这么做,而是直接在派生类对象地址上加上一个常数,获得虚基类实例的地址(virtual table offset strategy .)。
有一个很好的例子:
#include <stdio.h>
class A
{
public:
virtual int foo0(){
return 0;
}
char a[3];
};
class B: virtual public A
{
public:
virtual int foo1(){
return 1;
}
char b[3];
};
class C:virtual public B
{
public:
virtual int foo2(){
return 2;
}
char c[3];
};
int main(int argc, char* argv[])
{
A a;
B b;
C c;
printf(" sizeof A=%d, sizeof B=%d, sizeof C=%d /n",sizeof (A), sizeof (B), sizeof (C));
return 0;
}
在g++编译器里: sizeof A=8, sizeof B=16(A和B都有虚函数表,但B的虚函数表的负方面的地址里存着virtual base class(A)offset), sizeof C=24(C的虚函数表的负方面的地址里存着virtual base class(B)offset)
在VC++编译器里: sizeof A=8, sizeof B=20(A和B都有虚函数表,并且B还有一个virtual class base table pointer), sizeof C=32