多继承 菱形继承 虚继承

谈菱形继承前我们先来讨论多继承

一. 多继承:一个子类同时继承多个父类。

在这里插入图片描述

class A
{
public:
	A()
	{
		cout << "A::A" << endl;
	}
	~A()
	{
		cout << "A::~A" << endl;
	}
private:
	int m_a;
};
class B
{
public:
	B()
	{
		cout << "B::B" << endl;
	}
	~B()
	{
		cout << "B::~B" << endl;
	}
private:
	int m_b;
};
class C:public A,public B
{
public:
	C()
	{
		cout << "C::C" << endl;
	}
	~C()
	{
		cout << "C::~C" << endl;
	}
private:
	int m_c;
};
int main()
{
	C c;
	return 0;
}

我们可以在监视中看到c里面有A,B类的成员。
在这里插入图片描述

而且我们还可以看到构造顺序,先构造A,再构造B,最后构造C
在这里插入图片描述
问:为什么构造顺序是这样的呢?

原因:是因为在继承中,父类先构造,子类后构造,而A,B都是父类,那么构造的顺序即为声明继承的顺序
即 class C:public A,public B,先构造A类,后构造B类,而class C:public B,public A,是先构造B类,再构造A类。

二.菱形继承

菱形继承:是在多继承的基础上产生的。一个子类同时继承多个父类,而这多个父类又继承于另一个父类。
在这里插入图片描述

class A
{
public:
	A()
	{
		cout << "A::A" << endl;
	}
	~A()
	{
		cout << "A::~A" << endl;
	}
	int m_a;
};
class B:public A
{
public:
	B()
	{
		cout << "B::B" << endl;
	}
	~B()
	{
		cout << "B::~B" << endl;
	}
int m_b;
};
class C:public A
{
public:
	C()
	{
		cout << "C::C" << endl;
	}
	~C()
	{
		cout << "C::~C" << endl;
	}
	int m_c;
};
class D:public B,public C
{
public:
	D()
	{
		cout << "D::D" << endl;
	}
	~D()
	{
		cout << "D::~D" << endl;
	}
	int m_d;
};
int main()
{
	D d;
	return 0;
}

我们可以看到,在d对象中有两个A类,每个A类里都有一个m_a成员
在这里插入图片描述

而且我们可以看到会构造两次A对象,那么d中有两个m_a就很理所当然了。
在这里插入图片描述
问:此时如果我们给这个m_a赋值,会发生什么呢?
比如执行代码 d.m_a = 10;是无法通过编译的。而给出的错误消息是:对m_a的访问不明确。

原因:通过监视我们注意到,d中有两个m_a,一个是B中的,一个是C中的,而此时我们对m_a赋值,但是编译器是不知道我们要给哪个m_a进行赋值,所以出现报错:对m_a的访问不明确。

如果指明对B中的m_a赋值,我们需要加上限定

d.B::m_a = 10;//用类名限制访问
d.C::m_a = 20;
所以这就暴露出菱形继承的弊端:
1.会产生二义性;
2.数据会冗余,在内存中会存储多个变量。

解决菱形继承的办法----------------------------虚继承

三.虚继承

虚继承主要解决继承中访问不明确的问题
虚继承关键字:virtual

1.当虚继承存在的构造顺序
class A
{
public:
	A()
	{
		cout << "A::A" << endl;
	}
	~A()
	{
		cout << "A::~A" << endl;
	}
private:
	int m_a;
};
class B
{
public:
	B()
	{
		cout << "B::B" << endl;
	}
	~B()
	{
		cout << "B::~B" << endl;
	}
private:
	int m_b;
};
class C:public A,virtual public B
{
public:
	C()
	{
		cout << "C::C" << endl;
	}
	~C()
	{
		cout << "C::~C" << endl;
	}
private:
	int m_c;
};
int main()
{
	C c;
	return 0;
}

在这里插入图片描述
可以观察到,当B类是虚拟继承时,虽然继承顺序是A类在前,B类在后,但是真实的构造顺序是先构造B,再构造A,说明虚继承不遵守执行顺序

总结:当有普通继承和虚继承同时存在时,先构造虚继承的类,再构造普通继承的类

2.解决菱形继承暴露出来的问题
class A
{
public:
	A()
	{
		cout << "A::A" << endl;
	}
	~A()
	{
		cout << "A::~A" << endl;
	}
	int m_a;
};
class B:virtual public A
{
public:
	B()
	{
		cout << "B::B" << endl;
	}
	~B()
	{
		cout << "B::~B" << endl;
	}
int m_b;
};
class C:virtual public A
{
public:
	C()
	{
		cout << "C::C" << endl;
	}
	~C()
	{
		cout << "C::~C" << endl;
	}
	int m_c;
};
class D:public B,public C
{
public:
	D()
	{
		cout << "D::D" << endl;
	}
	~D()
	{
		cout << "D::~D" << endl;
	}
	int m_d;
};
int main()
{
	D d;
	return 0;
}

当B和C采用了虚继承,观察到
在这里插入图片描述
A类只构造了一次,而我们不采用虚继承时,A类构造了两次;
并且这个时候再对m_a赋值,比如d.m_a = 4,编译器不会报错了。那么是因为什么呢?我们去内存看一看

为了接下来好观察,我们分别对m_a,m_b,m_c,m_d初始化 1,2,3,4.

在这里插入图片描述
我们看到在内存中,m_a,m_b,m_c,m_d都可以看到,而我们知道数据是连续存储的。那么m_b左侧的地址和m_c左侧的地址是代表什么呢?
在这里插入图片描述
我们输出这两个地址看到在m_b左侧的地址存的值是20(转换为10进制),而我们从58地址向后数20个字节看到,指向的是01,即为m_a;m_c左侧的地址存的值是12(转换为10进制),我们从1c地址向后数12个字节看到,指向的是01,也为m_a。至此我们就可以得到结论了。

总结:虚拟继承了之后,访问m_a不会出错呢,实际上是因为B类和C类里面根本就没有m_a,自始至终m_a只有一个,那就是A中的m_a。B类和C类存储的是从当前地址指向m_a地址的偏移量。B类和C类共享m_a这个成员。

通过B,C这个地址找到m_a,这个地址叫虚基表指针

虚表指针指向的地址叫做虚基表

虚基表中存放的是偏移量通过偏移量可以找到父类的数据

结论:虚继承解决了菱形继承的二义性和数据冗余的问题,但是最好不要设计出菱形继承这样的结构,不然在复杂度和性能上都有问题。

面试题:

1.什么是菱形继承?菱形继承的问题是什么?

答:
菱形继承为一个子类继承于两个父类,而这两个父类又继承于另一个父类;
菱形继承会导致数据冗余和二义性。

2.什么是菱形虚拟继承?是如何解决冗余和二义性的?

答:
菱形虚拟继承是两个父类在继承另一个父类时采用虚拟继承;
是在两个父类中存储虚基表指针,虚基表指针指向的是虚基表,通过虚基表中的偏移量来访问最高父类的成员。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值