【C++从0到王者】第二十二站:一文讲透多继承与菱形继承

文章目录

  • 前言
  • 一、多继承
  • 二、菱形继承
  • 三、菱形虚拟继承
  • 四、菱形虚拟继承的底层原理
  • 五、菱形虚拟继承对于空间的优化
  • 六、多继承和菱形继承中的一些细节
  • 七、菱形继承在库里面的应用
  • 八、继承和组合
  • 九、继承总结


前言

在我们前面所说的继承其实在C++中也叫做单继承

即一个子类只有一个直接父类的时候称这个继承关系为单继承
在这里插入图片描述


一、多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述多继承即认为一个对象可能同时有其他两个或以上对象的属性所设计出来的。

class Student
{
protected:int _num; //学号
};
class Teacher
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{Assistant at;return 0;
}

如上代码所示,Assistant继承了Student,Teacher两个类的属性
在这里插入图片描述

二、菱形继承

虽然多继承看似很合理,但是多继承引发了一种新的问题——菱形继承

菱形继承是多继承的一种特殊情况
在这里插入图片描述

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

菱形继承导致的问题也正是如此。如下代码所示,就会产生二义性

在这里插入图片描述

此处的二义性,我们还可以通过类域指定访问去处理

在这里插入图片描述监视显示为
在这里插入图片描述

还有一种情形要注意:我们如果指定的是父类的父类的话,编译器是可以通过的
在这里插入图片描述但是此时的结果究竟指向哪个Student里的父类还是Person的父类呢?其实是跟继承的顺序有关的,我们写多继承的时候先是继承了Student,所以Student里面的Perosn中的_name会被修改为王五
在这里插入图片描述

三、菱形虚拟继承

在前面,我们得知了由多继承导致的菱形继承中的一些问题:数据冗余和二义性
为了解决这个问题,C++又出来了一个菱形虚拟继承

菱形虚拟继承只需要在菱形继承中的腰部位置(即Student和Teacher类)添加关键词virtual即可
在这里插入图片描述

在这里插入图片描述

有了菱形虚拟继承,我们可以不需要指定类域去访问_name,我们的Person在监视窗口看好像是存了三份,并且我们修改数据的时候三份同时进行修改。
在这里插入图片描述

在这里插入图片描述

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

四、菱形虚拟继承的底层原理

我们用如下代码进行研究

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

上面是一个普通的菱形继承,它的运行结果应为如下所示

在这里插入图片描述

这里我们要注意,我们看似这里好像是给包了起来。实际中是连续的内存进行存放的。
在这里插入图片描述

在这里插入图片描述

如上是菱形继承的底层内存分布

接下来看菱形虚拟继承的底层分布
我们还是上面的例子,只不过将其改为菱形虚拟继承。

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;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

这样的话,我们从监视窗口看好像是有三份,其实不然,这里并非三份。而是一份

在这里插入图片描述

我们从内存窗口可以更加精确的观测到内存的变化

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述

我们发现,a似乎只有一份,且对象模型发生非常大的变化
在这里插入图片描述

也就是说,现在的A既不在B也不在C。这里倒是还可以理解,因为为了解决数据冗余二义性,它需要放到其他位置上,具体方最上面还是最下面是取决于编译器自己规定的

但是B和C里面似乎又多了一些东西,这些东西又是什么呢?如下图青色部分所示
在这里插入图片描述

我们从模样上来看,这两个有点像指针
于是我们从内存中找到这里指向的位置,如下所示(注意是小端机器)
在这里插入图片描述在这里插入图片描述
我们可以注意到,这个指针指向的位置存的是一个0,然后下一个位置存储的是一个有效值
在这里插入图片描述

其中,前者为20,后者为12我们不难注意到以下关系。他们距离A的地址正好相差这么多

所以这里指针存储的是距离A的偏移量。那么现在可能我们会好奇,为什么要搞一个指针,不直接存的。直接存不可以吗?其实是可以的。但是为什么我们编译器没有这样做呢?

我们注意到我们的偏移量是存在第二个里面,第一个里面是0,这个0是为其他值预留的。如果还有其他值的话,可以直接存进来。 因为菱形虚拟继承的,可能不止这一个。
而且这里仅仅只是一个类型,我们可能要实例化很多个对象,每个对象如果都是直接存储的话,那么代价太大了,不如直接开辟一个空间将偏移量全部放进去,然后每个对象只有一个指针指向即可。

如下就是两个对象的时候,他们都是存储相同的指针
在这里插入图片描述

这里的映射,我们有时候也称之为虚基表(寻找基址偏移量的表)

而且在如下的场景下,我们的这里是菱形虚拟继承的情况下,不仅仅是D的对象模型是前面的样子,B的对象模型也是类似于D的,它有一个指针指向一个虚基表,然后存储一个偏移量,根据这个偏移量就可以找到A的部分
在这里插入图片描述

以及类似的下面指针的情形,切割的场景,也是类似的,这个指针指向的仅仅只有B的那一部分,pb指针是切割出来的。为了能够找到对应的_a,必须通过偏移量进行寻找
,不通过偏移量编译器控制不住
在这里插入图片描述

还有如下所示的样例

int main()
{B b;b._a = 1;b._b = 2;D d;d._a = 1;B* pb = &b;pb->_a++;pb = &d;pb->_a++;return 0;
}

后面的四行代码想必我们都已经了解了,由于此处是菱形虚拟继承,所以pb在寻找a的时候会先通过偏移量来进行找到的,之后我们将pb接收d的地址,此处就是子类给父类的切割了。就是我们上面的样例了。也就是说他们寻址的底层都是一样的。都是通过偏移量去找到的,我们看下面的汇编,也会发现是一模一样的
在这里插入图片描述

也就是说,在这里的情景就是无论如何,编译器始终先取到偏移量,然后计算出_a的地址,最后在访问。

五、菱形虚拟继承对于空间的优化

当我们不使用菱形虚拟继承的时候

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()
{cout << sizeof(D) << 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()
{cout << sizeof(D) << endl;return 0;
}

在这里插入图片描述

我们会发现,为什么菱形虚拟继承所消耗的空间比虚拟继承消耗的空间还大呢?菱形虚拟继承不是都已经解决了数据冗余了吗?

我们先分析一下我们的对象模型
下面是菱形继承的
刚好是五个整型,所以是20符合我们的计算结果
在这里插入图片描述下面是菱形虚拟继承的
可以看到,果然是24
在这里插入图片描述

不过这里其实是节省了的,总体而言相当于我们是节省四字节,但是花费了八字节
不过我们这里的八字节是恒定的,因为用的是指针,节省的四字节是B和C中的A是四字节的,但是又因为要在最底层多出一个A,所以总体就是节省了一个A类的对象。也就是节省了四字节

那么如果我们将A所消耗的空间变大,那么是不是从商业的角度来看,就开始盈利了。

在菱形虚拟继承下
我们先看当A的成员变量为100个元素的大小的时候,消耗420个空间,为8+8+4+400
在这里插入图片描述
在菱形继承下,为812个空间,为4+400+4+400+4
在这里插入图片描述

可见确实是由8个字节换取了A的成员变量的大小

六、多继承和菱形继承中的一些细节

我们看如下代码。试问p1、p2、p3之间的关系是什么

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

要知道这个问题,我们得先知道的一点是,多继承的对象存放中,先继承的在上面。

所以他们的指向关系为如下所示。首先前两个是切片,所以他们看到的就是他们类里面的部分,所以都指向自己的部分,而p3不是切片,它关心的是整个对象。所以也在最上面起始处。
在这里插入图片描述

所以最终为p1==p3!=p2
在这里插入图片描述

那么如果我们将继承顺序换一下,先继承Base2,然后继承Base1
在这里插入图片描述

最终结果如下所示

在这里插入图片描述

我们再来看这样一个题:
试问输出的结果是什么

class A {
public:A(const char* s) { cout << s << endl; }~A() {}
};
class B :virtual public A
{
public:B(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};
class C :virtual public A
{
public:C(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};
class D :public B, public C
{
public:D(const char* sa, const char* sb, const char* sc, const char* sd) :B(sa, sb), C(sa, sc), A(sa){cout << sd << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

对于这道题,它是一个菱形虚拟继承,我们要清楚它的对象模型

我们先猜一猜这个A会调用几次构造?是三次吗?其实不是的,它只调用一次构造。因为这是一个菱形虚拟继承,在这里面其实只有一份A,所以它也只能构造一次。因为一个对象不可能构造三次。

那么这个A对象何时调用呢,我们知道这个A它既不在B也不在C,它在D里面。
而我们new的时候调用的是D的构造函数,它先走的是初始化列表,这个初始化列表它走的顺序是声明的顺序。声明的顺序中,A是第一个,所以A虽然在最下面,但是它却是第一个先执行的,所以先打印A类,然后走B,在走B的时候并不会走这个A的构造,编译器会处理干净的。然后就是C,最后打印D
在这里插入图片描述

那么既然B和C的构造函数不会走A的构造,能否将其给去掉呢?其实是不可以的,因为我们有可能会单独调用B对象。

七、菱形继承在库里面的应用

虽然菱形继承很坑,我们一般不建议使用菱形继承,但是在库里面是有人玩过的

如下所示,下面的箭头都是继承,我们就会发现中间出现了菱形继承,iostream继承了istream和ostream。
在这里插入图片描述

八、继承和组合

我们已经知道什么是继承了,那么什么是组合呢?如下所示就是组合与继承的区别,组合其实就是一个自定义类型的成员变量

class A
{};
//继承
class B : public A
{};
//组合
class C
{
private:A _a;
};

我们可以看出,继承和组合都完成了对对象的复用

  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
  3. 优先使用对象组合,而不是类继承 。
  4. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  5. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
  6. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
  7. 一般符合is-a关系的使用继承,如植物和花。符合has-a的就使用组合,如轮胎和车。既符合继承又符合组合的一般使用组合。因为组合耦合度更低。

九、继承总结

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO(OO是面向对象,OOP是面向对象程序设计)语言都没有多继承,如Java。

本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/100928.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

田间气象站的优势与应用

在农业生产中&#xff0c;田间气象站是重要的气象监测工具&#xff0c;它能够对农田间的气象信息进行实时监测和记录&#xff0c;为农民伯伯提供农业生产科学依据。 田间气象站是由多个传感器共同组成&#xff0c;能够收集各项气象参数&#xff0c;包括我们常见的风速、风向、…

【计算机网络八股】计算机网络(一)

目录 计算机网络的各层协议及作用&#xff1f;TCP和UDP的区别&#xff1f;UDP 和 TCP 对应的应用场景是什么&#xff1f;详细介绍一下 TCP 的三次握手机制&#xff1f;为什么需要三次握手&#xff0c;而不是两次&#xff1f;为什么要三次握手&#xff0c;而不是四次&#xff1f…

Linux 计算机网络基础概论

一、网络基本概念 1、网络 网络是由若干节点和连接这些结点的链路组成&#xff0c;网络中的结点可以是计算机、交换机、路由器等设备。通俗地说就是把不同的主机连接起来就构成了一个网络&#xff0c;构成网路的目的是为了信息交互、资源共享。 网络设备有&#xff1a;交换机…

CSS中的display属性有哪些值?它们的作用?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ CSS display 属性的不同取值和作用1. block2. inline3. inline-block4. none5. flex6. grid7. table、table-row、table-cell8. list-item9. inline-table、table-caption、table-column 等 ⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#x…

云服务 Ubuntu 20.04 版本 使用 Nginx 部署静态网页

所需操作&#xff1a; 1.安装Nginx 2.修改配置文件 3.测试、重启 Nginx 4.内部修改防火墙 5.配置解析 6.测试是否部署成功 1.安装Nginx // 未使用 root 账号 apt-get update // 更新apt-get install nginx // 安装 nginx 1.1.测试是否安装没问题 在网页上输入云服务的公网…

头条移动端项目Day05 —— 延迟队列精准发布文章

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…

安卓图形显示系统

Android图形显示系统 Android图形显示系统是Android比较重要的一个子系统&#xff0c;和很多其他子系统的关联紧密。 Android图形系统比较复杂&#xff0c;这里我们从整体上理一遍&#xff0c;细节留待后期再去深入。Android图形系统主要包括以下几个方面&#xff1a; - 渲染…

司徒理财:8.21黄金空头呈阶梯下移!今日操作策略

黄金走势分析 盘面裸k分析&#xff1a;1小时周期的行情局部于1896附近即下行通道上轨附近录得一系列的K线呈震荡下行并筑圆顶&#xff0c;上轨压制有效&#xff0c;下行通道并未突破&#xff0c;后市建议延续看下行。4小时周期局部录得一系列的纺锤线呈震荡&#xff0c;但行情整…

组合总和-LeetCode

给你一个无重复元素的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的所有不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序返回这些组合。 candidates 中的同一个数字可以无限制重复被选取 。如果至少一个…

回归预测 | MATLAB实现SOM-BP自组织映射结合BP神经网络多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现SOM-BP自组织映射结合BP神经网络多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现SOM-BP自组织映射结合BP神经网络多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09;效果一览基本介绍…

【python办公自动化】使用PysimpleGUI实现AHP指标的添加和删除及编号重新排序

使用PysimpleGUI实现AHP指标的添加和删除 1 运行界面2 添加指标3 删除指标4 编码重新排序5 全部代码1 运行界面 2 添加指标 输入框中输入内容,点击“添加指标”按钮 然后就会自动添加到上方列表中 3 删除指标 选中要删除的指标,点击“删除指标”按钮 此时就把第三个选…

Protocol Buffers [protobuf]

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

牛股预测器V1.0实战(工银瑞信金融科技挑战赛排名第二)

全代码和数据关注公众号《三个篱笆三个班》免费提供&#xff01;一键可跑&#xff0c;每日选股。 对AI炒股感兴趣的小伙伴可加WX群&#xff1a; 赛题概述&#xff1a; 基于人工智能的量化选股投资策略建模挑战 任务描述&#xff1a; 通过数学和计算机技术分析市场数据&…

Docker容器无法启动 Cannot find /usr/local/tomcat/bin/setclasspath.sh

报错信息如下 解决办法 权限不够 加上--privileged 获取最大权限 docker run --privileged --name lenglianerqi -p 9266:8080 -v /opt/docker/lenglianerqi/webapps:/usr/local/tomcat/webapps/ -v /opt/docker/lenglianerqi/webapps/userfile:/usr/local/tomcat/webapps/u…

视觉SLAM:一直在入门,如何能精通,CV领域的绝境长城,

目录 前言 福利&#xff1a;文末有chat-gpt纯分享&#xff0c;无魔法&#xff0c;无限制 1 什么是SLAM&#xff1f; 2 为什么用SLAM&#xff1f; 3 视觉SLAM怎么实现&#xff1f; 4 前端视觉里程计 5 后端优化 6 回环检测 7 地图构建 8 结语 前言 上周的组会上&…

【AGC】Publishing api怎么上传绿色认证审核材料

【问题描述】 华为应用市场会对绿色应用标上特有的绿色标识&#xff0c;代表其通过华为终端开放实验室DevEco云测平台的兼容性、稳定性、安全、功耗和性能的检测和认证&#xff0c;是应用高品质的象征。想要自己的应用认证为绿色应用就需要在发布应用时提供绿色认证审核材料&a…

uniapp条形码实现

条形码在实际应用场景是经常可见的。 这里教大家如何集成uniapp条形码。条形码依赖类库JsBarcode. 下载JsBarcode源码&#xff0c;对CanvasRenderer进行了改进兼容uniapp。 import merge from "../help/merge.js"; import {calculateEncodingAttributes, getTotal…

三分钟解决AE缓存预览渲染错误、暂停、卡顿问题

一、清除RAM缓存&#xff08;内存&#xff09; 你应该做的第一件事是清除你的RAM。这将清除当前存储在内存中的所有临时缓存文件。要执行此操作&#xff0c;请导航到编辑>清除>所有内存。这将从头开始重置RAM缓存 二、清空磁盘缓存 您也可以尝试清空磁盘缓存。执行此操作…

武汉凯迪正大—变比组别测试仪

一、概述 在电力变压器的半成品、成品生产过程中&#xff0c;新安装的变压器投入运行之前以及根据国家电力部的预防性试验规程中&#xff0c;要求变压器进行匝数比或电压比测试。传统的变比电桥操作繁琐&#xff0c;读数不直观&#xff0c;且要进行必要的换算&#xff0c;测试时…

案例-基于MVC和三层架构实现商品表的增删改查

文章目录 0. 项目介绍1. 环境准备2. 查看所有2.1 编写BrandMapper接口2.2 编写服务类&#xff0c;创建BrandService&#xff0c;用于调用该方法2.5 编写Servlet2.4 编写brand.jsp页面2.5 测试 3.添加3.1 编写BrandMapper接口 添加方法3.2 编写服务3.3 改写Brand.jsp页面&#x…