多重继承的内存结构分析

1、普通的多继承情况

需要用类名加两个冒号:: 来说明成员所属的基类。

代码如下:

classA

{

public:

voidfun()

{

a= 2;

}

inta;

intx;

};

classB

{

public:

voidfun()

{

b= 5;

}

 

intb;

intx;

};

 

classC : public A, public B

{

public:

intc;

};

 

int_tmain(int argc, _TCHAR* argv[])

{

C*pc = new C();

pc->A::fun();

pc->B::fun();

 

return0;

}

我们从成员变量和成员方法两方面进行观察,

进入调试模式:查看C 对象的内容:


可以看到一个C对象实际上包含一个A对象,一个B对象,然后再加上自身特有的属性c,从A继承来的x 和从B继承来的x 并不在同一块空间中。但是由于他们的标识符(变量名)一样,所以要通过所在的基类名来说明。

注意:关于上图中的内容只是对象的逻辑模型,而不是真实的内存模型


pc->A::fun();

009A2DF3  mov        ecx,dword ptr [pc] 

009A2DF6  call       A::fun (09A1523h) 

pc->B::fun();

009A2DFB  mov        ecx,dword ptr [pc] 

009A2DFE  add        ecx,8 

009A2E01  call       B::fun (09A152Dh) 

关于方法的调用也是类似。 关于这种编译器的识别方式,我们稍作了解即可。重点在于跟后面的程序的实现原理。

 

 

2、使用虚基类的多重继承

在看虚基类的例子之前,我们看一下普通的有共同基类的情况

代码如下:

classA

{

public:

voidfunc()

{

a= 3;

}

inta;

};

 

classB1 : public A

{

public:

intb1;

};

 

classB2 : public A

{

public:

intb2;

};

 

classC : public B1, public B2

{

public:

intc;

};

 

 

int_tmain(int argc, _TCHAR* argv[])

{

 

C*pc = new C();

 

pc->B1::a= 5;

pc->B2::a= 6;

pc->b1= 1;

pc->b2= 2;

 

 

pc->B1::func();

pc->B1::func();

 

return0;

}

先看一下对象的内容,可以发现,它首先是由包含B1B2的对象,然后再B1 B2 中分别包含A对象。C20个字节。

然后再看一下方法的调用情况:

pc->B1::func();

000B68F2  mov        ecx,dword ptr [pc] 

000B68F5  call       A::func (0B1537h) 

pc->B2::func();

000B68FA  mov        ecx,dword ptr [pc] 

000B68FD  add        ecx,8 

000B6900  call       A::func (0B1537h) 

调用的方法都是A中的方法,但是在找到A的过程需要明确说明:是通过B1对象还是通过B2对象。

 

我们再看一下使用虚基类的情况

classA

{

public:

voidfunc()

{

a= 3;

}

inta;

};

 

classB1 : virtual public A

{

public:

intb1;

};

 

classB2 : virtual public A

{

public:

intb2;

};

 

classC : public B1, public B2

{

public:

intc;

};

 

int_tmain(int argc, _TCHAR* argv[])

{

 

C*pc = new C();

 

cout<< sizeof(C) << endl;

 

pc->B1::a= 5;

pc->a= 6;

pc->b1= 1;

pc->b2= 2;

 

pc->B1::func();

pc->func();

 

return0;

}

首先还是先看一下C对象的内容

发现它在原来的基础上又多了一个公共的基类对象A,此时C对象占用的空间为24个字节。需要注意的是,图中所示的成员变量只是逻辑意义上的。我们再来看一下内存中的实际数据。

B1B2 虚继承之后会在对象开始添加两个虚指针。真正从A继承来的部分在最后存放。

然后再看方法的调用,

pc->B1::func();

00E76950  mov        eax,dword ptr [pc] 

00E76953  mov        ecx,dword ptr [eax] 

00E76955  mov        edx,dword ptr [pc] 

00E76958  add        edx,dword ptr [ecx+4] 

00E7695B  mov        ecx,edx 

00E7695D  call       A::func (0E71537h) 

pc->func();

00E76962 mov         eax,dword ptr[pc]     //eax保存对象的首地址

00E76965 mov         ecx,dword ptr[eax]   // eax4个字节内容复制到ecx

00E76967 mov         edx,dword ptr[pc]    / / edx指向对象首地址

00E7696A add         edx,dword ptr [ecx+4] //  [ecx]处的内容是00000000, [ecx+4]处的内容是00000014(普通成员所占用的字节数),相加之后,edx指向虚基类的首地址。此时,[edx] = 6

00E7696D  mov        ecx,edx 

00E7696F  call       A::func (0E71537h) 

虚继承时,成员函数的调用方式不发生改变。

 

综上所述,在虚继承时,生成的对象的前四个字节保存一个地址,我们称它为虚指针,它指向一段保存当前对象结构数据的内存空间。我们可以称它为虚表,从而找到真正要读写和写入的变量。在后面看了多态的 内存结构之后,会更加清楚。

 

 

思考题:

classA

{

public:

voidfunc()

{

a= 3;

}

inta;

};

 

classB1 : public A

{

public:

intb1;

};

 

classB2 : public A

{

public:

intb2;

};

 

classC : virtual public B1, virtual public B2

{

public:

intc;

};

 

 

int_tmain(int argc, _TCHAR* argv[])

{

C*pc = new C();

 

pc->B1::a= 5;

pc->B2::a= 6;

pc->b1= 1;

pc->b2= 2;

 

pc->B1::func();

pc->B2::func();

 

return0;

}

这是我测试时,想到的一种情况。现在我们用之前分析的内容来分析一下C对象是如何存放的,以及虚表中会有哪些内容。

首先,这里B1B2 都是普通的继承。而C则虚继承于B1B2,那么B1B2C的虚基类。C的对象的前四个字节是一个虚指针,它指向一个虚表,虚表中应该存放两个虚基类对象相对于C对象首地址的偏移地址。

C对象内容如下:虚指针cccccccc(c 的空间,我们没有初始化) 05 00 00 00 01 00 00 00B1继承来的内容) 06 00 00 00 02 00 00 00 B2继承来的内容)他们的偏移地址是81610h

所以虚表中的内容应该是 00 00 00 00(前4个字节为0,可能是做验证用的,具体的以后再分析)08 00 00 00 0a 00 00 00

我们现在来验证一下

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值