C++中的【菱形虚继承】深入剖析

转载 2012年09月20日 11:08:33
        在C++的多重继承中,出现菱形状继承的情况下,在构造对象时的内存分布及构造函数的调用流程上出现了问题。

 

好了,直接切入正题,所谓的菱形继承,最简单的构造如下:

 

class A

{

public:

    A( void ) : nVar( 0xaaaa0000 ){}

 

public:

    int nVar;

};

 

class B1 : public A

{

public:

    B1( void ){}

};

 

class B2 : public A

{

public:

    B2( void ){}

}; 

 

class C : public B1, public B2

{

public:

    C( void ){}

};

 

就是这样一个多重继承,用图形化来表示之间的关系就是:

 

                           A

                         /   /

                        /     /

                      B1    B2

                       /      /

                        /    /

                          C

然后,在创建C的对象:

int main( void )

{

    C obj;

    return 0;

我想大家应该知道这样将造成什么情况,在这里可以清楚的知道obj的大小为8,为什么是8,先看内存分布:

假如obj的内存地址为0x0012ff18.

0x0012FF18:  00 00 aa aa 00 00 aa aa

 

看了obj对象的内存,里面有2个A的副本,红色的就是B1那条线继承下来的内存,蓝色就是B2那条线继承下来的。因此A的构造函数被调用了两次,这里B1在前面,B2在后面是因为一对多继承是从左到右分布内存的。

 

从这里明显知道这样的结局肯定是很悲剧的。更可怕的是假如使用obj访问nVar成员将导致编译出错:

obj.nVar = 0x100;

 

对nVar的访问不明确,因为有两个副本,编译器不知道你到底要修改那个副本,从而导致编译错误,这里访问成员函数也是一个道理。

 

那么,有什么解决办法不让这种现象出现呢,C++提出了虚继承,以解决这个问题:

 

class A

{

public:

    A( void ) : nVar( 0xaaaa0000 ){}

 

public:

    int nVar;

};

 

 

class B1 : virtual public  A

 

{

public:

    B1( void ){}

};

 

 

class B2 : virtual public A

 

{

public:

    B2( void ){}

}; 

 

class C : public B1, public B2

{

public:

    C( void ){}

};

 

这样继承下来后,A就只会保留一个副本,再来看内存分布(这里声明,我使用的是VC2008版本来测试的):

假如obj的内存地址为:0x0012FF10

0x0012FF10:  0041580c 00415800 aaaa0000

 

 

可以清晰看出这里0xaaaa0000只有一个,而这时前面多了两个值,obj的大小为12字节,前面蓝色的地址就是C类的虚基指针(vbtable)如果A有虚函数的话,在蓝色和红色之间还会加上虚函数表(vftable)这时就占16字节了。这里就不具体介绍多重继承的虚表的内存分布了。

 

好了,下面就是本文的重点了,来看看obj对象创建时,调用构造函数的流程:

流程大概就是:在obj创建时,首先会调用C类的构造函数,在构造函数中,首先会将两个vbtable的偏移赋值给前面的蓝色部分内存。之后就会调用A的构造函数,调用之后再调B1和B2的构造函数。

 

用伪代码来表示:

C()

{

    vbtable;

    vbtable;

    A::A();

    B1::B1();

    B2::B2();

}

 

那么在调用B1和B2的构造函数是时,按理说会调用A的构造函数,因为B1、B2也是继承于A,但是为什么没有调用A的构造函数呢?来看看反汇编代码:

 

首先看main函数:

    C obj;
004113DE  push        1    
004113E0  lea         ecx,[obj] 
004113E3  call        C::C (4110E6h)

 

在红色处调用C的构造函数,再来看C的构造函数:

00411460  push        ebp  
00411461  mov         ebp,esp 
00411463  sub         esp,0CCh 
00411469  push        ebx  
0041146A  push        esi  
0041146B  push        edi  
0041146C  push        ecx  
0041146D  lea         edi,[ebp-0CCh] 
00411473  mov         ecx,33h 
00411478  mov         eax,0CCCCCCCCh 
0041147D  rep stos    dword ptr es:[edi] 
0041147F  pop         ecx  
00411480  mov         dword ptr [ebp-8],ecx 
00411483  cmp         dword ptr [ebp+8],0 
00411487  je          C::C+47h (4114A7h) 
00411489  mov         eax,dword ptr [this] 
0041148C  mov         dword ptr [eax],offset C::`vbtable' (41580Ch) 
00411492  mov         eax,dword ptr [this] 
00411495  mov         dword ptr [eax+4],offset C::`vbtable' (415800h) 
0041149C  mov         ecx,dword ptr [this] 
0041149F  add         ecx,8 
004114A2  call        A::A (4110EBh) 
004114A7  push        0    
004114A9  mov         ecx,dword ptr [this] 
004114AC  call          B2::B2 (4110AAh) 
004114B1  push        0    
004114B3  mov         ecx,dword ptr [this] 
004114B6  add         ecx,4 
004114B9  call        B1::B1 (41107Dh) 
004114BE  mov         eax,dword ptr [this] 
004114C1  pop         edi  
004114C2  pop         esi  
004114C3  pop         ebx  
004114C4  add         esp,0CCh 
004114CA  cmp         ebp,esp 
004114CC  call        @ILT+330(__RTC_CheckEsp) (41114Fh) 
004114D1  mov         esp,ebp 
004114D3  pop         ebp  
004114D4  ret         4   

 

上面蓝色的为加粗字体,可以看出在赋值vbtable。下面的红色为加粗的部分就是调用A的构造函数。这不奇怪。

在调用A的构造之前有一句:add  ecx, 8 这一句的目的是为了将this定位到两个vbtable之后,在调用A的构造函数时,直接往this所指向的内存地址下写值:0xaaaa0000。因此就构成了布局:

0x0012FF10:     0041580c          00415800    aaaa0000

                        C::this/( vbtable)     vbtable         A::this

 

C的this在这里看当然是0x0012ff10,A的this就是0x0012ff18,中间相隔两个vbtable,其实this也就是某个类的起始地址,没有什么特别的。

 

到这里,你可能注意到了蓝色加粗和红色加粗的两条一样的指令push 0,这条语句显然是编译器添加的,B2的构造函数明显没有参数,这样push一个0进去有点类似隐含的一个参数,那么push一个0进去到底做了些什么呢,再看B1的构造函数:

 

00411550  push        ebp  
00411551  mov         ebp,esp 
00411553  sub         esp,0CCh 
00411559  push        ebx  
0041155A  push        esi  
0041155B  push        edi  
0041155C  push        ecx  
0041155D  lea          edi,[ebp-0CCh] 
00411563  mov         ecx,33h 
00411568  mov         eax,0CCCCCCCCh 
0041156D  rep stos    dword ptr es:[edi] 
0041156F  pop          ecx  
00411570  mov         dword ptr [ebp-8],ecx 
00411573  cmp         dword ptr [ebp+8],0 
00411577  je            B1::B1+3Dh (41158Dh) 
00411579  mov         eax,dword ptr [this] 
0041157C  mov         dword ptr [eax],offset B1::`vbtable' (415818h) 
00411582  mov         ecx,dword ptr [this] 
00411585  add         ecx,4 
00411588  call          A::A (4110EBh) 
0041158D  mov         eax,dword ptr [this] 
00411590  pop         edi  
00411591  pop         esi  
00411592  pop         ebx  
00411593  add         esp,0CCh 
00411599  cmp         ebp,esp 
0041159B  call        @ILT+330(__RTC_CheckEsp) (41114Fh) 
004115A0  mov         esp,ebp 
004115A2  pop         ebp  
004115A3  ret         4 

 

红色的那句指令很明显,ebp+8正是函数的第一个参数,这里虽然没有,但是压入了一个0,这样一个cmp与0比较相等,执行蓝色的跳转直接跃过A的构造函数调用到绿色的那条指令。这样便实现了只调用一次A的构造函数的功能。B2的构造函数也是同理,这里就不介绍了。

 

有了这样一个push 0 然后又检查是否为零的操作,所以就算你在B1、B2中显示调用A的构造函数,结果还是不会调用A的构造函数的。

形如: B1( void ): A(){} 因为判断为零直接跳转到构造函数的用户代码里。

 

好了,本文就到这里就差不多了,这里只是介绍了虚继承中构造函数调用的原理。望大家多多提意见哈。

C++继承和菱形继承中的虚继承

(1)C++继承概念: C++继承分为公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。在C++语言中,一个派生类可以从一个基类派生,也可以从多...
  • yx20130919
  • yx20130919
  • 2016年06月17日 12:17
  • 474

C++菱形继承与虚继承

虚继承,菱形继承
  • xiaolewennofollow
  • xiaolewennofollow
  • 2016年08月14日 12:20
  • 805

【C++基础之二十一】菱形继承和虚继承

菱形继承是多重继承中跑不掉的,Java拿掉了多重继承,辅之于接口。C++中虽然没有明确说明接口这种东西,但是只有纯虚函数的类可以看作Java中的接口。在多重继承中建议使用“接口”,来避免多重继承中可能...
  • jackyvincefu
  • jackyvincefu
  • 2014年01月05日 09:05
  • 10546

C++中的【菱形虚继承】深入剖析

转眼间有过了一个月了,自从【C/C++语言入门篇】连载结束后,已经很久没有写博了。最近一直忙着本科毕业论文和工作上的任务,加上一个对于我来说非常重要的事情正在进行中。所以近段时间脑子一直处于绷紧状态,...
  • langsim
  • langsim
  • 2015年01月24日 22:36
  • 366

带有虚函数的菱形继承和带有虚函数的菱形虚继承

对于某些函数来说,基类希望它的派生类定义适合自身的版本,此时基类就将这些函数声明为虚函数。 在存在虚函的类,创建对象时会产生虚表指针,虚表指针指向一个虚表,这时就可以通过虚表访问自己定义的函数。 ...
  • D_leo
  • D_leo
  • 2017年04月12日 19:10
  • 588

C++中虚继承的作用及底层实现原理

虚继承和虚函数是完全无相关的两个概念。 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可...
  • bxw1992
  • bxw1992
  • 2017年08月30日 22:26
  • 1505

由底层和逻辑深入剖析c++系列

在2013年大二暑假,我在学完汇编之后又学了一遍c++,准备用汇编反编译一下c++来了解其语言运作的底层奥秘,因此准备写一系列的文章,但是由于时间关系,只写了三篇。现在看这些文章,虽然有的地方写的不成...
  • zhzz2012
  • zhzz2012
  • 2015年06月03日 15:15
  • 808

初析菱形继承(不存在虚函数的菱形继承)

菱形继承的定义,及其缺点,并通过虚继承使得菱形继承的缺点得以消除。
  • superficial_
  • superficial_
  • 2017年02月14日 18:12
  • 236

STL深入剖析——————第二章:空间配置器

本章主要是空间配置器,一般我们都不用管的。不过弄懂这个对接下来的很多内容...
  • wso4133560
  • wso4133560
  • 2014年05月12日 16:10
  • 320

【C++】c++单继承、多继承、菱形继承内存布局(虚函数表结构)

c++单继承、多继承、菱形继承内存布局(虚函数表结构)
  • SuLiJuan66
  • SuLiJuan66
  • 2015年10月04日 18:44
  • 2976
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C++中的【菱形虚继承】深入剖析
举报原因:
原因补充:

(最多只允许输入30个字)