多重虚继承内存分析

符号说明

说明:大写字母代表类如 A,小写字母代表类中成员变量如 ma,斜杠和反斜杠代表有继承关系,继承的先后顺序是先左后右

最简单的菱形继承模型

菱形继承

继承关系简图

注:这里使用十六进制进行初始化,方便内存的查看

                       A(ma = 0x1)
     		 (public)/   \(public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
             (public)\   /(public)
                       C(mc = 0x3)

内存分析

下图是利用类C生成的对象c的内存分布,由于生成子类时可能会调用父类的成员变量或函数所以必须先构造父类,所以内存中都是先加载父类数据然后才是子类数据。
可以看到,先加载的是类B的数据,由于类A是B的父类,所以加载结果是(1,2),然后加载的是类BB的数据(1,22),最后是类C的数据(3)。
虚继承

菱形继承+爷爷类和第一个父类间虚继承

继承关系简图

                       A(ma = 0x1)
     (virtual public)/   \(public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
             (public)\   /(public)
                       C(mc = 0x3)

内存分析

下图是内存图,可以看到先加载的是,类B的虚基表指针(0x00967B38)和数据(2),然后加载的是类BB的数据(1,22),之后加载的是类C的数据(3),最后加载的是类B的父类A中的数据(1)。
这里可以看到,如果类BB不用虚继承就不会产生虚基表,即使同一个爷爷类也一样。
再观察虚基表中(内存2中)的数据,这里0x00967B38是“派生类对象的地址相对于虚基类表指针的偏移“,当虚基表指针不在首位的时候这个数值会变成非零数,后续会介绍!0x00967B3C保存着类B的父类A的首地址与类B在对象c中的首地址偏移量(后续就不解释了),这里数据为十六进制的14,即十进制的20,5个int型长度(由于这里所有类中的数据都是int型就简化了验证),从内存1中类B的首地址(即虚基表指针处)移动5个int刚好就到了0x007AFDD8的位置,也就是类B父类A的首地址位置。

菱形继承+爷爷类和第一个父类间虚继承

菱形继承+爷爷类和第二个父类间虚继承

继承关系简图

                       A(ma = 0x1)
     		 (public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
             (public)\   /(public)
                       C(mc = 0x3)

内存分析

先加载的是,类B的数据(1,2),然后加载的是类BB的虚基表指针(0x00F47B38)和数据(22),之后加载的是类C的数据(3),最后加载的是类B的父类A中的数据(1)。
菱形继承+爷爷类和第二个父类间虚继承

菱形继承+爷爷类和两个父类间虚继承

继承关系简图

                       A(ma = 0x1)
     (virtual public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
             (public)\   /(public)
                       C(mc = 0x3)

内存分析

先加载的是,类B的数据,虚基表指针(0x005C7B48)和数据(2),然后加载的是类BB的虚基表指针(0x005C7B54)和数据(22),之后加载的是类C的数据(3),最后加载的是类B的父类A中的数据(1)。
这里可以看出当有共同的父类时类B和类BB的虚基表中的偏移量都会从对应的虚基表指针处指向同一个A类首地址
菱形继承+爷爷类和两个父类间虚继承

这里可以思考一个问题:为什么会把虚继承的父类数据放到子类C的后面?

因为爷爷类A的数据放到类B中或者类BB中都不合适(如果放到了一个类中那么调整了继承顺序后,内存结构会变化很大),只能找个公共地方,类C的专有数据之后是个合理的地方,有天然的分割线(类C的数据),这只是我当前的想法,供大家参考。

菱形继承+爷爷类和第一个父类间虚继承+第一个父类和子类间虚继承

继承关系简图

                       A(ma = 0x1)
     (virtual public)/   \(public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     (virtual public)\   /(public)
                       C(mc = 0x3)

内存分析

先加载的是类A数据(1)和类BB数据(22),类BB的数据!注意这里的继承顺序,是先继承的类B但是由于类B是虚继承,所以需要先加载普通继承的数据这里就先加载类BB的数据了,所以类B的数据就放到后面了。之后加载的是类C的专有数据,类C的虚基表指针(0x00C47B3C,这个是类C虚继承于类B所生成的)和数据(3),之后加载的是类B的父类A的数据(1,由于虚继承所以放到了后面加载),最后加载的是类B的专有数据,类B的虚基表指针(0x0047B4C,这个是类B虚继承于类A所生成的)和数据(2)。
先看类C的虚基表(内存2中)处。第一个数据是(fffffff8是有符号整型-8在内存中的表示)-8,从虚基表指针处(0x009EF8B8),偏移-8个字节(向前两个int)刚好是c对象的地址&c(0x009EF8B4),第二个数据8是类C中虚基表指针与类B的父类A的首地址的偏移量,转换后指向向爷爷类A的首地址(0x009EF8C4),这个是从类B中继承过来的。第三个数据是类C中虚基表指针与类B数据的首地址的偏移量,即类B的虚基表指针(0x009EF8C8)。
再看类B的虚基表(内存2中的部分,内存3中的开始),这里就能看出,同一类中的虚基表“可能”都是放在一起的(具体的操作应该是看编译器的实现方式吧,这里不确定),虚基表中第一个数据是,该指针对自己类首地址的偏移量,第二个数据是对类B的父类A的偏移量(ffffffffc是有符号整型-4在内存中的表示)。
菱形继承+爷爷类和第一个父类间虚继承+第一个父类和子类间虚继承

菱形继承+爷爷类和第二个父类间虚继承+第二个父类和子类间虚继承

继承关系简图

                       A(ma = 0x1)
		     (public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
		     (public)\   /(virtual public)
                       C(mc = 0x3)

内存分析

注意!下面的内存是否似曾相识呢?和上例比较,不能说一模一样,只能说是完全没有差别!这里肯定体现出来虚继承的加载机制了,都是先加载普通继承的数据,因为虚继承的数据都是放在最后面的,虽然有加载顺序的区别!
菱形继承+爷爷类和第二个父类间虚继承+第二个父类和子类间虚继承

菱形继承+爷爷类和第一个父类间虚继承+第二个父类和子类间虚继承

继承关系简图

                       A(ma = 0x1)
     (virtual public)/   \(public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
		     (public)\   /(virtual public)
                       C(mc = 0x3)

内存分析

先加载的是,类B的数据,类B的虚基表指针(0x00DE7B3C)和数据(2),然后加载的是类C的数据(3),
之后加载的是类B中的爷爷类A的数据(1),最后加载的是虚继承的类BB的数据,BB的父类A的数据(1)和类BB自己的数据(22)。
再看类B的虚基表(内存2中),虚基表中第一个数据是,该指针对自己类首地址的偏移量,第二个数据是类B的虚基表指针对类B的父类A的偏移量,第三个数据其实是类C的(类C虚继承于类BB所产生的),这里与类B共用了一个虚基表指针(下面有假设为什么会这样),通过偏移量指向了类BB的首地址(0x004FF7F4)。
菱形继承+爷爷类和第一个父类间虚继承+第二个父类和子类间虚继承

这里要注意下,明明我类C是虚继承类B的为什么这里的类C没有虚基表指针?

这里给出一个假设:因为当加载完类B的数据后,类C前没有空间分配了,全都占满了(如果在类C前加一个指针大小的话会改变类B虚基表中的偏移量!),所以为了不改变数据结构,就复用类B的虚基表指针,所以插在了类B的虚基表的最后也就是第三个数据位置处(0x00DE7B44)来记录如何通过类C来找到类BB。

菱形继承+爷爷类和第二个父类间虚继承+第一个父类和子类间虚继承

继承关系简图

                       A(ma = 0x1)
   		     (public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     (virtual public)\   /(public)
                       C(mc = 0x3)

内存分析

这个结果和上面的又是类似,但有所不同。先加载的是,类BB的数据,类BB的虚函数表指针(0x00797B3C),和类BB的数据(22),这里先加载BB是因为类C是虚继承与类B所以需要将类B的所有数据放到类C后面,所以需要先加载没有虚继承的类BB的数据(这里因为是类C的对象所以只看与类B和类BB的继承关系),这里也就说明了为什么在虚基表中先写入的是类BB与类A的虚继承偏移量(0x00797B40的14即十进制的20,5个int)。然后加载的是类C的数据(3),之后加载虚继承类B的所有数据,类B的父类A的数据(1),类B的数据(2),最后加载的是类BB的父类A的数据(1)。
菱形继承+爷爷类和第二个父类间虚继承+第一个父类和子类间虚继承

菱形继承+所有继承都是虚继承

继承关系简图

                       A(ma = 0x1)
   	 (virtual public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     (virtual public)\   /(virtual public)
                       C(mc = 0x3)

内存分析

接下来就是最后一个案例了,坚持住!首先加载的是类B的数据,由于是爷爷类与父类间也是虚继承,所以会先加载一个虚基表指针(0x007C7B44),因为所有的继承关系都是虚继承,所以之后加载的是类C的数据(3)。之后由于类B虚继承于类A所以加载的是类A的数据(1)。接下来就是类B的数据,虚基表指针(0x007C7BF4)加自己的专有数据(2)。最后加载类BB的数据,由于类B与类BB都虚继承与类A,所以类B与类BB会公用一个类A的数据(0x010FF7C8),最后加载的是类B的虚基表指针(0x007C7BFC)和类B的专有数据(22)。
看类C的虚基表(内存2中)处,先加载的是从类B寻找的爷爷类A的偏移,然后加载的是对类B的偏移,最后加载的是对类BB的偏移。
看类B的虚基表(内存3中)处,加载的是类B的虚基表指针处与类A的偏移。
看类BB的虚基表(内存4中)处,同理,加载的是类BB的虚基表指针处与类A的偏移。
菱形继承+所有继承都是虚继承

代码

如下是普通的菱形继承的验证代码,其余情况可以由它衍生,就不放上来了,都类似。

#include <iostream>
//爷爷类
class A
{
public:
	int ma = 0x1;
};
//第一个父类
class B :public A
{
public:
	int mb = 0x2;
};
//第二个父类
class BB :public A
{
public:
	int mbb = 0x22;
};
//子类
class C :public B, public BB
{
public:
	int mc = 0x3;
};

int main()
{
	C c;
	return 0;
}

总结

虚继承的加载方式和普通继承的顺序相同,但必须先加载非虚继承的数据,其中如果这些类中有虚继承,就会先执行这些类中的虚继承,但只是虚基表中的数据在前面,对象中的数据还是按照继承顺序加载。
一旦有虚继承,那么在内存中原先存放父类的数据的地方会转换为一个虚基表指针(32位中是4个字节),指向父类地址,这样也就把数据分离了。

思考一下其他几种继承关系的内存加载情况

               		   A(ma = 0x1)
 		  	 (public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     (virtual public)\   /(virtual public)
                       C(mc = 0x3)
                       A(ma = 0x1)
   	 (virtual public)/   \(public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     (virtual public)\   /(virtual public)
                       C(mc = 0x3)
                       A(ma = 0x1)
   	 (virtual public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     		 (public)\   /(virtual public)
                       C(mc = 0x3)
                       A(ma = 0x1)
   	 (virtual public)/   \(virtual public)
    	 (mb = 0x2) B	  BB(mbb = 0x22)
     (virtual public)\   /(public)
                       C(mc = 0x3)

如何使用内存窗口

请看:Visual Studio的几个小技巧

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值