说明:本文给出的结论均是在VS2010下调试的结果。
一、问题引入
下面的四个类是典型的C++虚继承的基本结构,现在的问题是这四个类对象的sizeof分别是多少?
class Base{ //虚基类 public: double dou; }; class Derived1 : public virtual Base{ //虚继承 public: double in; }; class Derived2 : public virtual Base{ //虚继承 public: double on; }; class A : public Derived1, public Derived2{ };
在VS2010下,内存对齐设置为4字节对齐,运行以下代码的结果:
int main() { int i = 0xaabbccdd; double a = 1, b = 2, c = 3, d = 4; Base bobj; bobj.dou = 1; Derived1 d1obj; d1obj.dou = 2; d1obj.in = 1; Derived2 d2obj; d2obj.dou = 3; d2obj.on = 1; A aobj; aobj.dou = 4; aobj.in = 1; aobj.on = 1; cout << sizeof(bobj) << endl; cout << sizeof(d1obj) << endl; cout << sizeof(d2obj) << endl; cout << sizeof(aobj) << endl; system("pause"); }
二、虚基类的内存布局
main函数中的变量i是为了快速定位到栈的存储位置,变量a,b,c,d只是是为了给出double类型的1,2,3,4在内存中的形式,方便后面跟踪各个类对象的成员在内存中的位置。下面的截图是在debug win32下启动调试后得到的栈上的内存布局:
说明:
1、栈上的每个变量间都加入了8个字节的cccccccc cccccccc,这是debug模式下编译器插入的security cookie;
2、黑色框表示的是Base bobj的内存空间,占8字节,存放了double型的变量bobj.dou = 1;
3、红色框表示的是Derived1 d1obj的内存空间,占20个字节:开始的4个字节(0x01387838)是指向虚基类表的指针,后面的八个字节(0x00000000 3ff00000)是double型变量d1obj.in = 1,最后八个字节(0x00000000 40000000)是从虚基类Base继承而来的double型变量d1obj.dou = 2;
4、同理3,黄色框表示的是Derived2 d2obj的内存空间,也是占20个字节:开始的4个字节(0x0138789c)是指向虚基类表的指针,后面的八个字节(0x00000000 3ff00000)是double型变量d2obj.on = 1,最后八个字节(0x00000000 40080000)是从虚基类Base继承而来的double型变量d2obj.dou = 3;
5、绿色框表示的是A aobj的内存空间,共32字节:
开始4个字节(0x013878b4)对应了从Derived1类继承的虚基类表指针,随后的八个字节(0x00000000 3ff00000)是从Derived1中继承的double型变量aobj.in = 1,接下来四字节(0x013878a8)对应了从Derived2类继承的虚基类表指针,之后的八个字节(0x00000000 3ff00000)是从Derived2中继承的double型变量aobj.on = 1,最后八个字节(0x00000000 40100000)则是继承自虚基类Base的double型变量aobj.dou
= 4。
观察以上的内存布局恶意得出以下结论:
1、在虚继承体系中的派生类内存布局的次序是:虚基类表指针,派生类本身的非static成员变量,继承至虚基类的非static成员变量。虚基类指针放在最前面,而从虚基类继承来的成员则在最后面;
2、类A的对象aobj中确实只持有一份虚基类的成员变量,并没有因同时继承了Derived1和Derived2,而持有两份;那么,如果去掉虚继承,改为普通的基础,aobj的内存布局又会是怎样呢?(去掉代码中的两个virtual关键字,调试一下内存布局,可以发现d1obj,d2obj,aobj的内存空间没有虚基类表指针;如果在代码中用到aobj.dou,会编译报错,说“dou的访问不明确”)
看到这里,大家也许会有疑问:凭什么说对象的首字节就是虚基类表指针呢?编译器又是怎么通过虚基类表指针控制A的对象只持有一份虚基类的成员变量的呢?接下来,给出相应的汇编指令加以说明。
三、汇编指令
先看Derived1对象相关部分的汇编代码:
说明:
1、执行完构造函数后,虚基类指针即安放好了;
2、d1obj的头四个字节的内容(0x000A7838)给eax,指向完后发现eax = 686136,也就是16进制的0x000A7838;
3、地址0x000A7838 + 4的内容如下图所示:
4、5、将2放到了d1obj首地址偏移12(0x0000000c)个字节的位置上,也就是对象d1obj内存的最末端。
也就是说:虚基类表中存放了虚基类的成员在派生类内存空间中的偏移量。
再看看A类对象aobj的相关汇编代码:
可以发现大致流程和上一段汇编代码差不多,而且确实只通过头四个字节(从Derived1继承的虚基类指针)取出偏移18字节(aobj对象末尾)的dou变量赋值为4。
eax = 0x000a78b4,[eax + 4]的内容如下:
最后再看看构造函数中是如何安放虚基类表指针的,以Derived1类的构造函数为例: