菱形虚拟继承对象内存模型详解

目录

一、菱形继承概念及其缺点

二、解决菱形继承问题

三、对象模型探究

1、存在菱形继承问题的对象模型

2、菱形虚拟继承的对象模型

3、B类对象模型

四、继承与组合

五、总结


一、菱形继承概念及其缺点

单继承: 一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

因为有多继承的存在,导致出现菱形继承问题。C++语言作为编程语言老大哥(也是第一个吃螃蟹的)无法避免的的踩中了这个大坑。后来的一些面向对象编程语言也是吸取C++的经验,取消了多继承的语法,如Java等。

菱形继承: 属于多继承中的一种特殊情况


 

菱形继承的问题: 从下图对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题。
在Assistant的对象中Person成员会有两份

#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
	string _name;
};
class Student :public Person
{
protected:
	int _num;
};
class Teacher :public Person
{
protected:
	int _id;
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};
int main()
{
	Assistant a;
	a.Student::_name = "bruse";
	a.Teacher::_name = "peter";
	return 0;
}

可以通过显示指定访问哪个父类的成员解决二义性问题,但是数据冗余问题依然无法解决

二、解决菱形继承问题

通过虚拟继承可以解决菱形继承的二义性和数据冗余的问题

#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
	string _name;
};
class Student :virtual public Person
{
protected:
	int _num;
};
class Teacher :virtual public Person
{
protected:
	int _id;
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};
int main()
{
	Assistant a;
	a._name = "bruse";
	a._name = "peter";
	return 0;
}

此时问题已经解决,但对于对象模型中数据是如何存储的,我们依然不知道!

三、对象模型探究

下面将使用一个简化的菱形继承继承体系来研究虚拟继承原理

1、存在菱形继承问题的对象模型

 从上图可以清晰的看出,D类对象d中存在两份_a数据,存在数据冗余和二义性的问题

2、菱形虚拟继承的对象模型

从上图可以看出_a数据在该对象中仅存储了一份,解决了冗余的问题。

但二义性的问题又是如何解决的呢?对象模型中存储的009a7bdc和009a7be4两串十六进制数字又有什么用呢?

D类对象d中其实存在两个指针,分别属于B域和C域,009a7bdc和009a7be4两串十六进制数字便是这两个指针的值。这两个指针也被称为虚基表指针,其指向虚基表。

 虚基表中存储的是偏移量,可以通过偏移量找到唯一的_a数据。

可能会有人会发现一个问题,虚拟继承不是可以解决冗余问题吗?怎么观察上述的对象模型,使用了虚继承后怎么反而比不使用虚继承还用多上了四个字节?

解答:上述的对象模型都是通过简化的菱形继承继承体系得来的,虚基类中只存有一个int类型数据。若虚基类中存有1000个元素的int数组,不使用虚继承就会存储两份int[1000],而使用就只用存储一份。

3、B类对象模型

由上面可以看出,是否采用虚继承会影响D类对象的对象模型以及数据存储方式,那么B类和C类的对象会受到影响吗?

由上图可以看出,B类对象中也同样存在虚基表指针指向记录偏移量的虚基表

但是B类对象不存在数据的冗余和二义性,为什么对象模型也会设计成如此呢?

#include<iostream>
#include<string>
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 B, public C{
public:
	int _d;
};
void func(B& data) {
	cout << data._b << endl;
}
int main()
{
	D d;
	d._a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	B bb;
	bb._a = 1;
	bb._b = 2;
	func(d);
	func(bb);
	return 0;
}

当出现上述代码的情况时,B类对象可以作为参数传递给func函数,D类对象也可以发生切片作为参数传递给func函数。编译器无法分辨你所传参数是B类对象还是D类对象,为了保证调用方法的一致性,B类和D类的对象模型也保持相同的设计方式。

四、继承与组合

1.public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

2.组合是一种has-a的关系。假设B组合了A,每个B对象中都有A对象

3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)
术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。
派生类和基类间的依赖关系很强,耦合度高。

4.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。
对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse)
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。
优先使用对象组合有助于你保持每个类被封装。

5.实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承也可以用组合时,优先使用组合

五、总结

总的来说,尽量避免菱形继承的使用,大量使用菱形虚拟继承也会导致程序效率降低  (底层多了通过虚基表指针访问虚基表中记录的偏移量,再通过偏移量寻找公共数据的步骤),但也无伤大雅,毕竟现在的CPU性能也十分强大了。

在C++中文件IO的设计中便出现了菱形继承

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GG_Bond20

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值