如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱。这个C++中最复杂的继承层次在VS上的实现其实我没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不到什么理由来解释它,也只算是知其然不知其所以然吧。
虚、实基类都没有虚函数
这种情况也还算比较简单。因为虚函数表指针一定是会放在最开始的,所以根据猜测也可以知道其大概布局情况。看下面一个简单的例子
#pragma pack(8) class F1{ public: int f1; F1():f1(0xf1f1f1f1){} }; class A : virtual F1{ public: inta; A():a(0xaaaaaaaa){} virtual void funA2(){} virtual ~A(){} virtual void funA(){} }; int _tmain(int argc, _TCHAR* argv[]) { A aa; return 0; }
这是只有一个虚继承的简单情况,可以看到类A的前4个字节就是虚函数表指针,虚函数表里面有3个项,当然就是分别指向类A的那3个虚函数的指针了(注意是小端序):
可以看到 funA2() 函数的入口地址是0x00f81028;~A() 函数的入口地址是 0x00f811bd;
funA() 函数的入口地址是 0x00f810aa。
为了验证 funA2() 函数的入口地址是否正确(~A()和 funA() 可以用相同的方法验证),打开反汇编窗口:
找到地址为 0x00f81028 的地方:
可以看到 0x00f81028 存放着一条 jmp 指令,跳转到A::funA2() 函数的真正地址:0x00f815d0,我们接着到 0x00f815d0 处看看是什么情况
可以看到 0x00f815d0 处的确是A::funA2() 函数开始执行的地方,懂汇编的童鞋应该知道前两句指令就是建立A::funA2() 函数的堆栈~
可以看到虚基类表里面的两个偏移,一个是 -4,说明类A的起始地址位于虚基类表指针偏移-4 个字节的地方,也就是虚函数表指针的地址了。虚基类表里面的第二项就是第一个虚基类( F1 )相对于虚基类表指针的偏移,是8 字节,可以看到刚好也是 f1f1f1f1 的位置。
看了一个简单的情况我们再来看一个稍微复杂的继承情况,虚实多继承,所有的基类仍然都没有虚函数。
#pragma pack(8) class F0{ public: char f0; F0() :f0(0xf0){}}; class F1{ public: int f1; F1():f1(0xf1f1f1f1){} }; class F2{ public: int f2; double f22; F2():f22(0), f2(0xf2f2f2f2){} }; class F3 :virtual F2{ public: int f3; F3(): f3(0xf3f3f3f3){} }; class F4 :virtual F2{ public: int f4; F4(): f4(0xf4f4f4f4){} }; class A : virtual F0, F1, virtual F3, F4{ public: inta; A():a(0xaaaaaaaa){} virtual void funA2(){} virtual ~A(){} virtual void funA(){} }; int _tmain(int argc, _TCHAR* argv[]) { A aa; return 0; }
我们知道,只要出现虚继承,那么派生类的内存布局就分为实部和虚部,我们来仔细分析一下这 64 个字节:
1. 派生类的实部:
1.1、74 68 ad 00:派生类 A 的虚函数表指针;(距派生类 C 的虚基类表指针 -16 个字节)
1.2、cc cc cc cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 -12 个字节)
1.3、f1 f1 f1 f1:实基类 F1 的成员变量,int f1 = 0xf1f1f1f1;(距派生类 C 的虚基类表指针 -8 个字节)
1.4、cc cc cc cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 -4 个字节)
1.5、84 68 ad 00:实基类 F4 和派生类 A 共用的虚基类表指针;(距派生类 C 的虚基类表指针 0 个字节)
1.6、f4 f4 f4 f4:实基类 F4 的成员变量,int f4 = 0xf4f4f4f4;(距派生类 C 的虚基类表指针 +4 个字节)
1.7、aa aa aa aa:派生类 A 的成员变量,int a = 0xaaaaaaaa;(距派生类 C 的虚基类表指针 +8 个字节)
实部的内存布局图如下:
2. 派生类实部和虚部的结合处:
cc cc cc cc:这四个字节是为了字节对齐而填充的,同时,这四个字节之前的内容是派生类的实部,之后的内容就是派生类的虚部;(距派生类 C 的虚基类表指针 +12 个字节)
3. 派生类的虚部:
3.1、f0 cc cc cc:虚基类 F0 的成员变量,char f0 = 0xf0,三个 cc 是为了字节对齐而填充的;(距派生类 C 的虚基类表指针 +16 个字节)
3.2、cc cc cc cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 +20 个字节)
3.3、f2 f2 f2 f2:虚基类 F2 的成员变量,int f2 = 0xf2f2f2f2;(距派生类 C 的虚基类表指针 +24 个字节)
3.4、cc cc cc cc:为了字节对齐而填充的 4个字节;(距派生类 C 的虚基类表指针 +28 个字节)
3.5、00 00 00 00 00 00 00 00:虚基类 F2 的成员变量,double f22 = 0x00000000;(距派生类 C 的虚基类表指针 +36 个字节)
3.6、98 68 ad 00:虚基类 F3 的虚基类表指针;(距派生类 C 的虚基类表指针 +40 个字节)
3.7、f3 f3 f3 f3:虚基类 F3 的成员变量,int f3 = 0xf3f3f3f3;(距派生类 C 的虚基类表指针 +44 个字节)
虚部的内存布局图如下:
以上就是派生类 A 的内存布局
然后看下派生类 A 和其基类 F4 共用的虚基类表:
按照规则这个共用的虚基类表里面的项是这样安排的:
1. 第一项是0,表示F4的起始地址就是虚基类表指针的起始地址;
2. 第二项是F4的虚基类F2相对于虚函数表指针的偏移,是0x18(十进制 24);
3. 第三项开始就不是F4的虚基类的项了,从第三项开始的各个项是类A的其他虚基类,按照声明的顺序排放,所以第三项就是虚基类F0 相对于虚基类表指针的偏移,是0x10(十进制 16);
4. 然后F1没有虚部跳过,再到 F3,因为F3的虚部F2已经包含在了之前的项里面了所以也跳得,所以第四项就到F3 的偏移,是0x28(十进制 40)个字节;
接下来我们看看 F3 的虚基类表:
F3的虚基类表是它自己独享的,先是自己起始地址的偏移,然后是虚基类的偏移,可以看到它的虚基类F2 相对于它这个虚基类表指针的偏移值-16(24 - 40)个字节,也是正确的。