菱形虚拟继承详解

本文详细探讨了菱形继承、菱形虚拟继承及其包含虚函数的情况,解释了虚继承如何解决二义性和数据冗余问题,并通过内存布局分析了虚基表和虚表在对象中的作用。总结了虚函数在菱形继承结构中的存储位置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

菱形继承是继承里面比较复杂的有一种,在这里我分别对菱形继承、菱形虚拟继承、菱形虚拟继承含虚函数这几种情况

一、菱形继承

看下图就是一个菱形继承


#include<iostream>
using namespace std;

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public C, public B
{
public:
	int _d;
};

int main()
{
	D dd;
	cout << sizeof(dd) << endl;

	dd.B::_a = 1;
	dd._b = 3;

	dd.C::_a = 2;
	dd._c = 4;

	dd._d = 5;
	B bb;
	C cc;
	cout << sizeof(bb) << endl;

	return 0;
}



从内存监视口可以发现dd占用20个字节,但是通过视窗口可以明显发现C类和B类继承的同一个A类的成员_a的值不一样,这就产生了二义性,同一个成员怎可么可以同时又两个值呢?

 

所以为了解决在菱形继承或者多继承中都会出现的二义性和数据冗余的问题,就提出来虚继承,下面来看看虚继承是怎么实现的..

 

二.菱形虚拟继承

在子类继承基类时,通过virtual关键字就可以虚继承,下面我们通过内存布局来看看虚继承是怎么处理数据二义性的...

#include<iostream>
using namespace std;

class A
{
public:
	int _a;
};

class B :virtual public A
{
public:
	int _b;
};

class C :virtual public A
{
public:
	int _c;
};

class D : public C, public B
{
public:
	int _d;
};

int main()
{
	D dd;
	cout << sizeof(dd) << endl;

	dd._a = 1;
	dd._b = 3;

	dd._a = 2;
	dd._c = 4;

	dd._d = 5;
	B bb;
	C cc;
	cout << sizeof(bb) << endl;

	return 0;
}


这里可以发现 虚继承的dd对象和bb对象都比普通继承之前多了四个字节的空间,说明这四个字节的空间肯定和虚继承有关,下面我们看看内存分布来了解虚继承...




从监视窗口就可以发现C类和B类继承的同一个A类的成员_a的值现在的值是一样的,都是第二次赋值后的值2. 下面我们看看虚继承是怎么实现C类和B类公用一个成员_a



从上图可以发现对象dd的内存布局与虚继承之前有很大的区别,首先cc对象和bb对象的内存空间中都分别多了一个存储着一个地址的空间,而把它们_a变量放在了成员变量的最底下,使_a成为一个公共的变量.



那么cc对象内存中的0x011ceae8bb对象内存中的0x011cdbb0这两个地址指向的空间存的是什么呢?看下图这两个地址就是ccbb对象的虚基表地址,虚基表里存储的是偏移量,cc对象和bb对象就可以通过它们自己的虚基表中的偏移量,来找到公共的基类,然后调用A里的_a


总结:

1.虚继承解决了在菱形继承里面子类对象包含多分父类对象的数据冗余现象和二义性。

2.虚拟继承体系看起来好复杂,但是我们实际应用中很少会定义如此复杂的菱行虚拟继承体系,一般不到万不得已不要定义菱形虚拟继承结构体系,虽然解决了二义性,但也带来了数据冗余的现象。



三.菱形虚拟继承中包含虚函数

在看看下面代码,我在上面代码的基础上再加了些虚函数,让我我们来理解菱形继承里的虚表

#include<iostream>
using namespace std;

//定义一个可以指向对象里函数的函数指针
typedef void(*func)();

//打印虚函数表
void printvtable(int* vtable)   
{
	cout << "虚表地址>" << vtable << endl;

	for (int i = 0; vtable[i] != 0; ++i)
	{
		printf("第%d个虚函数地址:0x%x,->", i, vtable[i]);
		func f = (func)vtable[i];
		f();
	}
	cout << endl; 
}

class A
{
public:
	int _a;

	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}
};

class B :virtual public A
{
public:
	int _b;
	virtual void func2()
	{
		cout << "B::func2()" << endl;
	}
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}
};

class C :virtual public A
{
public:
	int _c;

	virtual void func1()
	{
		cout << "C::func1()" << endl;
	}
};

class D : public C,public B
{
public:
	int _d;

	virtual void func1()
	{
		cout << "D::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "D::func3()" << endl;
	}
};

int main()
{
	D dd;
    B bb;
	C cc;
	A aa;

	cout << sizeof(dd) << endl;

	dd._a = 1;
	dd._b = 3;

	dd._a = 2;
	dd._c = 4;

	dd._d = 5;

	cout << sizeof(bb) << endl;

	//D类里继承B类的虚表
	cout << "D:bb:0x" << ⅆ
	int* vtabledd = (int*)(*(int*)&dd); 
	printvtable(vtabledd);

	//D类里继承C类的虚表
	cout << "D:cc:0x" << ((int*)&dd + 3);
	int* vtablecc = (int*)(*((int*)&dd + 3));
	printvtable(vtablecc);


	//D类里继承A类的虚表
	cout << "D:aa:0x" << ((int*)&dd + 6);
	int* vtableaa = (int*)(*((int*)&dd +6));
	printvtable(vtableaa);
	return 0;
}


要清楚虚表和虚基表是两个完全不同的概念

虚表:虚函数表,存的是虚函数->多态

虚基表:存的是偏移量,解决二义性和数据冗余性。

 

好了,那么我上面我还提出了一个问题,就是虚基表里上面的地址为什么存着一个寻找对象自己的虚表的偏移量?下面我们解决

 

提醒:因为我代码改动后,再次编译内存中对象的地址数值会变化,但不影响大家看内存是怎样分布的和虚基表的作用。(仅下面这个图的地址数值和上面输出结果不一样)

下面我们来看看菱形继承中含虚函数后dd对象的内存分布是什么样子?是sizeof(dd)为什么是32看下图就知道了



从上图可以发现,当给bb对象里加了个fun2函数后发现dd的对象模型又多了四个字节,这四个字节是在dd对象中继承的bb的内存空间中多出来的,就是用来存放bb对象自己的虚表地址。

因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bbcc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。

然后dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。

 

 

下面看看菱形继承dd对象里的虚函数都分别在什么地方存储的:


通过上面的内存分布我总结:

1.dd对象继承重写aa对象的虚函数存储在继承来的aa对象的虚函数表里。

2.dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。

3.因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bbcc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。


下面我根据总结上面总结画出了一份菱虚拟继承的对象模型



如有错误之处,请多多指点














评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值