【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理

这篇具有很好参考价值的文章主要介绍了【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 单继承

在上一篇文章中,我们给大家演示的其实都是单继承。

单继承的概念:

单继承:一个子类只有一个直接父类的继承关系为单继承
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

2. 多继承

然后呢C++里面还支持多继承,那什么是多继承呢?

一个子类有两个或以上直接父类时称这个继承关系为多继承
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
比如一个类表示汽车,另一个类表示飞机。现在你希望创建一个新的类,使得它既可以像汽车一样在地上跑,又可以像飞机一样在天上飞,即这个新的类继承这两个基类的属性和行为,同时拥有汽车和飞机的特性。那这就是一个多继承。

2.1 多继承中指针偏移问题

然后多继承这里有一个题我们顺便做一下:

下面说法正确的是( )
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
大家先自己思考一下

其实很简单,就是我们之前讲过的切片嘛:

这里把子类对象d的地址分别赋给这三个指针
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
谁先被继承,对象模型里谁就在前面,这个后面我们会带大家观察对象模型。
所以应该是p1 == p3 != p2,选C

3. 菱形继承

多继承也不难理解,但是有时候可能会引发一些难搞的情况。

比如,多继承就有可能导致菱形继承的出现:

菱形继承是多继承的一种特殊情况。

那顾名思义,菱形继承就是继承关系近似呈一个菱形形状,比如像这样:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
简单解释一下,首先这里有一个Person类,然后Student继承了Person,Teacher也继承了Person。
然后,又有一个Assistant(助教)类即继承了Student,又继承了Teacher。
那此时它们的继承关系就呈一个菱形状。

那菱形继承会导致什么问题呢?

3.1 菱形继承的问题——数据冗余和二义性

菱形继承就会存在一个数据冗余和二义性的问题

从下面的对象成员模型构造可以看出,在Assistant的对象中Person成员会有两份。
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

我们通过程序来带大家看一下:

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; // 主修课程
};

在这样一个菱形继承体系里面,就会存在如下一些问题:

首先呢,由于Student和Teacher都是继承Person,所以它们都拥有Person的_name属性,然后呢,Assistant又同时继承了Student和Teacher,所以就会导致在Assistant里面有两份_name,那这就导致了一个数据冗余的问题。
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
由于Assistant里面有两个_name,一个继承自Student,一个继承自Teacher,所以在访问的时候就会发生歧义,我们把它叫做数据二义性
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
我们现在想给Assistant的类对象a的_name成员赋值,那这里就无法确定你访问的是哪一个。

那有办法解决这个问题吗?

当然也有办法,我们可以通过显式指定访问哪个父类的成员来一定程度的解决二义性的问题
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
但是数据冗余的问题依然存在。
而且我们这里方便演示只给Person搞了一个成员_name,如果再多一些属性,比如住址、电话、年龄,性别等,那这样是不是都出现两份了。

那为了更好的解决菱形继承导致的数据冗余和二义性的问题,C++就引入了虚拟继承…

3.2 解决方法——虚拟继承

C++引入了虚拟继承可以解决菱形继承的二义性和数据冗余的问题

那虚拟继承是怎样的呢?

虚拟继承要用到一个新的关键字——virtual(虚拟的)
那怎么做呢?
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
给继承关系中第二层的类增加一个关键字virtual就行了。

然后就可以了吗?我们来看一下:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
这里调式窗口看起来还是多个,但其实它们是同一个(这里只是调试窗口展示出来的效果)
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
我们后面也会给大家讲一下底层的原理。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

3.3 虚拟继承的原理

为了研究虚拟继承的原理,我们下面给出一个简化的菱形继承的继承体系,再借助内存窗口(因为监视窗口已经看不出来底层真实的样子了)观察对象成员的模型

现在我们给出这样一个继承体系:

class A
{
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

那下面我们就一起来通过内存窗口分析一下虚拟继承的原理。

首先我们先来观察一下不使用虚继承时菱形继承底层是什么样子的:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
现在我创建一个D类的对象d,把它所有的成员(自己的包括继承下来的),这里我们给的都是整型(方便观察),将他们置成1到5的数值。
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
另外我们可以看到D先继承B,再继承C,在对象模型里面也是B在上面,C在下面的(就是我们上面提到的,谁先被继承,对象模型里谁就在前面)。

那我们接下来看看虚继承是怎么解决这个问题的:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
同样的程序,我们再来观察内存空间:
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
我们发现此时整个对象的模型已经发生改变了。
我们看到,原本BC里面都存有一份_a,但是现在_a只有一个,而且单独放在最后面,那此时d对象中就只有一个_a成员了,就不存在数据冗余了,访问的时候也没有二义性了。
但是,此时BC里面原本应该存_a的位置存的是个什么东西呢?
是随机值吗?
看着也不像啊。
🆗,那告诉大家这里面存的其实是两个地址或者说指针,那它们指向的空间又存的是什么,我们可以再开两个内存窗口观察一下(注意vs上是小端存储)
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
我们观察这两个指针指向的内存空间,发现它们指向的当前位置那一个字节都是0,但是下一个字节都是一个确定的数值,一个是20(窗口显示的16进制,我们转成10进制),一个是12

那存的这些数字又分别代表什么呢?

其实仔细观察可以发现原本BC中应该存_a的位置和现在_a所在的位置,它们之间的偏移量(相对距离)就是20和12!!!
因为我们现在设置的一行刚好4个字节
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

所以,它底层原理是这样的:

原来B里面有一个_a,C里面有一个_a,这就造成了数据冗余和访问时的二义性。
所以要解决这个问题,就要只存一个_a,那就不能存到原来的位置了。
怎么办?
它放到了一个公共的位置(这个位置的_a同时属于BC两个类),那怎么找到这个_a存放的位置呢?
原来存放_a的位置就存了两个指针(叫做虚基表指针),它们分别指向一块区域(我们把它叫做虚基表),这里面就存储了原来_a在BC中的存储位置到现在_a位置的一个偏移量,通过这个偏移量就可以找到现在_a所在的位置。

那大家可能有这样的疑问,在这里也提一下:

那大家可能会想,为什么不直接存_a的地址呢?为什么还要存一个指针,通过指针去找偏移量,再通过偏移量找_a。
🆗,我们上面也说了,指针其实指向一张表,它其实指向的并不是一个位置,而是一块区域,这里面可能存了多个有用的值,一般这种我们把它叫做表(在这里名字为虚基表),另外我们其实也发现这个偏移量并没有存在指向的第一个位置
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
第一个位置是0,偏移量在后面放。
那第一个位置其实是空出来有其他用处,跟后面的多态有关系。

另外呢:

其实这里D的上一层比如说B就也是这种结构
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
因为他这里为了保持一个统一处理,正常情况下B只继承A,是不会出现数据冗余的,但这里做了统一处理。

那什么情况下会去使用偏移量找这个公共的_a呢?

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
大家看这种场景
这个是我们上一篇文章讲过的赋值转换嘛,正常情况下我们可以认为它进行一个切片嘛,把d里面属于B类的那一部分直接切出来赋给b就可以了。
但是现在虚拟继承这种情况,b里面还有从A类继承下来的_a成员是不是不在B里面啊,而是单独放到了外面,那此时要找这个_a是不是的通过虚基表指针指向的虚基表里面的偏移量找啊。

当然对于我们当前举的这个例子来说

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
我们的A这个类搞的比较小,这样一看虽然解决了数据冗余的问题,但反而多费了4个字节的空间。
但是如果A这个类比较大的话,这样处理的优势就出来了,解决了数据冗余的问题,而且节省了很多空间。

那这里对于上面的那个Person的菱形虚拟继承体系我们也给了一个原理解释图,大家可以看一下:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

3.4 相关笔试题练习

下面我们来做一道这里相关的一个笔试题:

下面程序输出结果是什么?

class A {
public:
	A(const char* s)
	{ 
		cout << s << endl; 
	}
	~A() {}
};
class B :virtual public A
{
public:
	B(const char* s1, const char* s2)
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2)
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};
class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const char* s4)
		:B(s1, s2), 
		C(s1, s3), 
		A(s1)
	{
		cout << s4 << endl;
	}
};
int main() 
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

选项:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

大家先自己认真看一下这段代码,思考一下程序输出结果是什么?

🆗,我们来分析一下:

那我们观察这段代码会发现其实它还是一个菱形虚拟继承嘛,每个类里面都有一个构造函数。

然后现在我们在main函数里面new一个D对象,然后题目问我们程序输出什么?

首先告诉大家这道题的答案是A
为什么是A呢?
这里在main函数里面new了一个D嘛,所以这里会调用D的构造函数
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
那我们看它的初始化列表,这里的顺序是B、C、A。
但是,我们说了初始化列表初始化的顺序与初始化列表里面写的顺序是无关的,而于它们声明的先后次序是一致的。
那大家记住,在这里谁先被继承,谁就先被声明
所以这里肯定是先构造A,因为D继承BC,但BC先继承了A。
然后D又先继承了B,后继承了C。
所以构造的顺序应该是ABCD,答案选A.
那大家可能会想BC初始化不是也会调用A的构造初始化自己里面A的部分吗,那这里会打印三个ClassA吗?
是不会的,因为这里菱形虚拟继承,整个D里面只有一份A,BC公用一份A,所以这里A用自己的构造函数构造一次就可以了。
因此应该是class A class B class C class D
我们验证一下:
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
没问题。

我们如果把初始化列表的顺序换一下,那也还是A,因为跟初始化列表的顺序无关,而是跟声明的顺序有关:
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

除非我们把继承顺序换了:
【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言

因为我们说这里继承的顺序就是声明的顺序

4. 继承和组合

这是继承

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。基类的内部细节对子类可见。

除了继承还有一种关系叫做组合。

组合呢是这样的:

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言
其实就是一个类用另一个类的类对象作为其成员。
组合其实也是一种复用
组合是一种has-a的关系。假设D组合了C,每个D对象中都有一个C对象。C对象的内部细节对D是不可见的。

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

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

5. 继承的反思和总结

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

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

6. 用到的代码


//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 = "peter";
//
//	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
//	a.Student::_name = "xxx";
//	a.Teacher::_name = "yyy";
//	/*cout << &a.Student::_name << endl;
//	cout << &a.Teacher::_name << endl;
//	cout << &a._name << endl;*/
//
//	return 0;
//}

//class A
//{
//public:
//	int _a;
//};
class B : public A
//class B : virtual public A
//{
//public:
//	int _b;
//};
class C : public A
//class C : virtual public A
//{
//public:
//	int _c;
//};
//class D : public B, public C
//{
//public:
//	int _d;
//};
//int main()
//{
//	D d;
//	B b = d;
//	C c = d;
//
//	d.B::_a = 1;
//	d.C::_a = 2;
//	d._b = 3;
//	d._c = 4;
//	d._d = 5;
//	return 0;
//}


class A
{};

class B : public A
{};

class C
{};

class D
{
private:
	C _c;
};

【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理,C++,c++,开发语言文章来源地址https://www.toymoban.com/news/detail-588438.html

到了这里,关于【C++】复杂的菱形继承 及 菱形虚拟继承的底层原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++练级之路】【Lv.12】继承(你真的了解菱形虚拟继承吗?)

    快乐的流畅:个人主页 个人专栏:《C语言》《数据结构世界》《进击的C++》 远方有一堆篝火,在为久候之人燃烧! 继承(inheritance),是面向对象的三大特性之一。 它是面向对象编程中, 使代码可以复用 的最重要的手段,它允许程序员在 保持原有类特性的基础上进行扩展

    2024年03月14日
    浏览(48)
  • 【继承】复杂的菱形继承

    博主首页:  有趣的中国人   专栏首页:  C++进阶   本篇文章主要讲解 菱形继承   的相关内容 目录 1. 继承与友元 2. 继承与静态成员 3. 复杂的菱形继承及菱形虚拟继承 3.1 继承分类 3.2 菱形继承导致的问题 3.3 虚拟继承解决数据冗余的原理 4. 继承和组合的区别 友元关系不

    2024年04月22日
    浏览(37)
  • C++类开发的第六篇(虚拟继承实现原理和cl命令的使用的bug修复)

    接上一篇末尾虚拟继承的简单介绍之后,这篇来详细讲一下这个内存大小是怎么分配的。 cl 是 Microsoft Visual Studio 中的 C/C++ 编译器命令。通过在命令行中键入 cl 命令,可以调用 Visual Studio 的编译器进行编译操作。 cl 命令提供了各种选项和参数,用于指定源文件、编译选项、

    2024年03月09日
    浏览(46)
  • C++:面向对象大坑:菱形继承

    单继承:一个子类只有 一个直接父类 时称这个继承关系为单继承。 图示: 多继承:一个子类有 两个或以上直接父类 时称这个继承关系为多继承。 图示: 1.概念 菱形继承:菱形继承是多继承的一种特殊情况。即:一个类是另外几个类的子类,而这几个子类又是另外一个类

    2024年04月27日
    浏览(37)
  • 想要入坑C++?当我拿出菱形虚拟继承,阁下又该如何应对

    🌸作者简介: 花想云 ,目前大二在读 ,C/C++领域新星创作者、运维领域新星创作者、CSDN2023新星计划导师、CSDN内容合伙人、阿里云专家博主、华为云云享专家致力于 C/C++、Linux 学习 🌸 本文收录于 C++系列 ,本专栏主要内容为 C++ 初阶、C++ 进阶、STL 详解等,专为大学生打造

    2024年02月07日
    浏览(39)
  • 【浅尝C++】继承机制=>虚基表/菱形虚继承/继承的概念、定义/基类与派生类对象赋值转换/派生类的默认成员函数等详解

    🏠专栏介绍:浅尝C++专栏是用于记录C++语法基础、STL及内存剖析等。 🎯每日格言:每日努力一点点,技术变化看得见。 我们生活中也有继承的例子,例如:小明继承了孙老师傅做拉面的手艺。继承就是一种延续、复用的方式。C++为了提高代码的可复用性,引入了继承机制,

    2024年04月10日
    浏览(48)
  • C++--菱形继承

    1.什么是菱形继承         单继承:一个子类只有一个直接父类时称这个继承关系为单继承                                            多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承 菱形继承的问题:菱形继承有数据冗余和二义性

    2024年02月15日
    浏览(38)
  • 【C++】万字一文全解【继承】及其特性__[剖析底层化繁为简](20)

    前言 大家好吖,欢迎来到 YY 滴C++系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁 主要内容含: 欢迎订阅 YY 滴C++专栏!更多干货持续更新!以下是传送门! YY的《C++》专栏 YY的《C++11》专栏 YY的《Linux》专栏 YY的《数据结构》专栏 YY的《C语言基础》专栏 YY的《初学者易

    2024年02月01日
    浏览(77)
  • 云计算底层原理(虚拟化技术介绍,平台安装,网络)

    云服务   共建智能世界云底座-华为云 kvm    linux 内核模块 虚拟化   --- 划分资源 云计算  服务IAAS云   QEMU   虚拟化仿真工具 libvirt  虚拟化管理的接口 工具 IP地址配置方式不同   DHCP ,DNS 检测是否支持虚拟化 grep  -P \\\"svm|vmx\\\" /proc/cpuinfo 一 , 安装虚拟化平台 创建虚拟网

    2024年03月13日
    浏览(50)
  • 【C++】继承的基本特性(定义,赋值转换,友元,静态成员,虚拟继承,默认成员函数,作用域)

    🌏博客主页: 主页 🔖系列专栏: C++ ❤️感谢大家点赞👍收藏⭐评论✍️ 😍期待与大家一起进步! 它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。 Person是父类,也称作基类。Student是子类,也称作派生类 总结: 基类private成员

    2024年02月14日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包