C++虚继承的实现方式与内存布局

说明:本文给出的结论均是在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类的构造函数为例:


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。