深度探索之C++多重继承与虚拟继承

C++中在构造类似下面这种继承关系时,


(其中箭头表示父类到子类的继承关系)

我们可以对类以下面的形式进行声明:

class A 
{
public:
int _ma;
virtual void Tst1(){printf("A::Tst1\n");}
virtual void Tst2(){printf("A::Tst2\n");}
};
class B : virtual public A
{
public:
virtual void Tst1(){printf("B::A::Tst1\n");}
};


class C : virtual public A
{
public:
virtual void Tst2(){printf("C::A::Tst2\n");}
};


class D : public B, public C
{
//
};

注意上面的类继承声明中用到了virtual关键字,该关键字用于声明这是一个虚拟继承。如果去掉该关键字会发生什么呢?那么在D类中,按照正常继承情况下的内存布局,无疑将包含两份A类的占据内存,假设代码中添加D类对象访问_ma成员的代码,那么编译器将无法定位_ma的地址,因为它存在两份实例,且这在函数调用时也会产生令编译器模凌两可(ambigous)的错误,因为是调用(B::*)还是(C::*)不得而知。


接下来,编写几行测试代码,并根据其汇编代码,我们可以了解C++编译器(VC 6.0)在实现虚拟继承时所用到的特殊手段:

为了便于后面的分析,我这里直接给出D的内存模型,同样这是我自己根据汇编代码的出来的:


D one;

 push        1 ;
 lea         ecx,[ebp-10h]  ;这里说明sizeof(D) == 10h(16 = sizeof(_ma) + 3 * sizeof(vptr))
 call        @ILT+25(D::D) (0040101e) ;这里调用构造函数
A* pA = &one;

lea        eax,[ebp-10h] ;%eax = address one

test       eax,eax;测试指令,执行and 操作

jne        main+32h (0040d9c2)

mov     dword ptr [ebp-20h],0
jmp       main+3Fh (0040d9cf)

mov       ecx,dword ptr [ebp-10h];%eax = address one

mov       edx,dword ptr [ecx+4];

lea         eax,[ebp+edx-10h];

mov       dword ptr [ebp-20h],eax;

mov       ecx,dword ptr [ebp-20h];

mov       dword ptr [ebp-14h],ecx ;

这里应该注意的是pA的内容并不与变量one的首地址相同,代码中进行了调整:

(1)获取(B::vptr)所指向的第二个表项的内容,该内容存储着偏移量(mov         edx,dword ptr [ecx+4];)(该内容在实际中为8)

(2)获取one中属于A部分的基地址(lea         eax,[ebp+edx-10h];)

(3)将(2)中获得的地址存储在变量pA 中(mov         dword ptr [ebp-14h],ecx ;)

B* pB = &one;

lea         edx,[ebp-10h]
mov         dword ptr [ebp-18h],edx

从这里可以看出pB指针的值即为one变量的地址,这也说明在D的内存分布的起始端的内容为B
C* pC = &one;

lea         eax,[ebp-10h]
lea         ecx,[ebp-0Ch]
mov         dword ptr [ebp-24h],ecx
mov         edx,dword ptr [ebp-24h]
mov         dword ptr [ebp-1Ch],edx

(1)获取one中C部分的内存地址(lea         ecx,[ebp-0Ch])

(2)存储到pC中(mov         dword ptr [ebp-1Ch],edx)

所以根据上面的分析,我们可以知道D类对象的内存布局为:


接下来分析一下函数调用的过程,这主要涉及到虚表的操作:首先将整个程序所用到的栈内存布局画出来,个别内存的内容还未知,但不影响分析



pA->Tst1();

mov         eax,dword ptr [ebp-14h]
mov         edx,dword ptr [eax]
mov         esi,esp
mov         ecx,dword ptr [ebp-14h]
call        dword ptr [edx]

(1)首先获得pA指针所指向的内存的内容(mov         eax,dword ptr [ebp-14h]),该内容实际为D的虚表地址

(2)获得虚表指针所指向内存的首4字节内容(edx,dword ptr [eax])

(3)调用D::Tst1(动态绑定技术),调用成员函数时,其this指针默认通过exc寄存器传递,这里我们可以看出在D::Tst1的成员函数中this指针的值并不为one的地址
pA->Tst2();

mov         eax,dword ptr [ebp-14h]
mov         edx,dword ptr [eax]
mov         esi,esp
mov         ecx,dword ptr [ebp-14h]
call        dword ptr [edx+4]

调用D::Tst2成员函数,其过程与调用D::Tst1过程类似,但其函数函数地址在D的虚表中第二项,这样验证了上图中右上角所描绘的虚表内容
pB->Tst1();

mov         eax,dword ptr [ebp-18h]
mov         ecx,dword ptr [eax]
mov         edx,dword ptr [ebp-18h]
add         edx,dword ptr [ecx+4]

mov         eax,dword ptr [ebp-18h]
mov         ecx,dword ptr [eax]
mov         eax,dword ptr [ecx+4]
mov         ecx,dword ptr [ebp-18h]
mov         eax,dword ptr [ecx+eax]
 mov         esi,esp
mov         ecx,edx
call        dword ptr [eax]

(1)获得pB的内容(mov         eax,dword ptr [ebp-18h]; mov         ecx,dword ptr [eax]),它是一个虚表指针

(2)获得虚表的第二项内容(mov         eax,dword ptr [ecx+4]),它是一个偏移量,它等于D对象的虚表指针离到B部分起始地址的距离

(3)通过偏移量定位到虚表的地址(mov         eax,dword ptr [ecx+eax])

(4)传递this指针,这里它的值同样不等于one的起始地址,而是one内存块中B部分起始地址(通过edx寄存器传递,在前4句汇编代码中获得)

(5)调用D::Tst1函数

/下面代码的分析与上面类似
pB->Tst2();

mov         ecx,dword ptr [ebp-18h]
mov         edx,dword ptr [ecx]
mov         ecx,dword ptr [ebp-18h]

add         ecx,dword ptr [edx+4]
mov         eax,dword ptr [ebp-18h]
mov         edx,dword ptr [eax]
mov         eax,dword ptr [edx+4]
mov         edx,dword ptr [ebp-18h]
mov         eax,dword ptr [edx+eax]
mov         esi,esp
dword ptr [eax+4]

//
pC->Tst1();

mov         ecx,dword ptr [ebp-1Ch]
mov         edx,dword ptr [ecx]
mov         ecx,dword ptr [ebp-1Ch]
add         ecx,dword ptr [edx+4]
mov         eax,dword ptr [ebp-1Ch]
mov         edx,dword ptr [eax]
mov         eax,dword ptr [edx+4]
mov         edx,dword ptr [ebp-1Ch]
mov         eax,dword ptr [edx+eax]
mov         esi,esp
call        dword ptr [eax

/

pC->Tst2();

mov         ecx,dword ptr [ebp-1Ch]
mov         edx,dword ptr [ecx]
mov         ecx,dword ptr [ebp-1Ch]
add         ecx,dword ptr [edx+4]
mov         eax,dword ptr [ebp-1Ch]
mov         edx,dword ptr [eax]
mov         eax,dword ptr [edx+4]
mov         edx,dword ptr [ebp-1Ch]
mov         eax,dword ptr [edx+eax]
mov         esi,esp
call        dword ptr [eax+4]

相关的优秀博文:http://blog.csdn.net/littlehedgehog/article/details/5442430



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值