C++大坑之——多继承(菱形继承)

在这里插入图片描述

文章目录

  • 前言
  • 一、多继承是什么?
    • 1. 多继承概念
    • 2. 多继承语法
  • 二、菱形继承
    • 1. 为什么会有菱形继承问题?
    • 2. 代码感受菱形继承
    • 3. 虚拟继承
      • 1)虚拟继承概念及语法
      • 2)虚拟继承的原理
    • 4. 为什么要有虚基表?
    • 5. 为什么要有偏移量?
    • 6. 关于解决数据冗余
  • 三、小试牛刀
  • 四、库里的菱形继承
  • ❤️继承的总结和反思


前言

前面学习了继承的概念与语法,今天我们一起来看看C++中的大坑——菱形继承🥰


一、多继承是什么?

1. 多继承概念

多继承是指一个类可以同时继承多个父类的特性。在这种情况下,子类能够访问和使用其所有父类的方法和属性。

这样理解:现实生活中,子类可能会继承多个父类,比如骡子是由马和驴所生的,他同时继承了马和驴的一些特征。
在这里插入图片描述

这种特性在一些面向对象编程语言(如C++)中是允许的,但在其他语言(如Java)中则被限制为单继承。

我们再通过下面这个例子区分一下单继承和多继承:

这是单继承,一个子类只有一个直接的父类,他也只有这一个直接父类的成员
在这里插入图片描述

这是多继承,及子类同时具有两个及以上的直接父类,他有所有直接父类的成员
在这里插入图片描述


2. 多继承语法

多继承的基本语法是:class 子类 : 继承方式 父类1,继承方式 父类2…

现在有这样一种情况:

#include<iostream>
using namespace std;class Student
{
public:int _num; //学号int _age; //年龄
};class Teacher
{
public:int _id; // 职工编号int _age; //年龄
};class Assistant : public Student, public Teacher
{
public:string _majorCourse; // 主修课程int _age; //年龄
};int main()
{Assistant as;as.Student::_age = 18;as.Teacher::_age = 30;as._age = 19;return 0;
}

他是这样继承的,如下图所示:
在这里插入图片描述
也可以很清楚的看到,这里有三份年龄都不一样,这就是多继承


二、菱形继承

1. 为什么会有菱形继承问题?

假设有这样一个继承的样子:
在这里插入图片描述
两个类同时继承一个父类,他们呢又有同样一个子类,就会形成菱形继承

我们来看一下菱形继承的对象模型:

这会导致什么现象呢?其实刚刚多继承的那个例子已经有所铺垫了,作为子类,他有两个基类的成员,就造成了(1)数据冗余,(2)二义性

在Assistant的对象中Person成员会有两份。
在这里插入图片描述

这三种形式都属于菱形继承,
也就是说继承只要形成了闭环,就是菱形继承!
在这里插入图片描述


2. 代码感受菱形继承

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

直观的感受,这里有一个很大的问题:
在这里插入图片描述

假设我要使用a._name,就会有二义性,无法具体确定_name访问的是哪个父类成员的_name,但是二义性好解决,指定作用域就可以。但是,对于数据冗余的问题依然解决不了。


3. 虚拟继承

1)虚拟继承概念及语法

因此,就出现了虚拟继承!

虚拟继承是一种在C++中解决菱形继承问题的机制。当一个子类通过多个父类继承同一个祖先类时,会导致潜在的二义性(即“钻石问题”)。虚拟继承通过确保只有一份祖先类的实例存在,来避免这种问题。

主要特点:

  1. 语法:在继承时使用关键字virtual来声明父类。例如:

    class A {};
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {};
    

    注意,这里是在腰部进行virtual关键字,最下面的儿子以及祖先都不写!

  2. 共享实例:虚拟继承确保无论通过哪个路径继承,只有一个A的实例存在于D中。

  3. 构造顺序:虚拟基类的构造函数在所有派生类构造之前被调用,确保它的成员被初始化。

  4. 访问:在虚拟继承中,派生类可以通过虚拟基类来访问祖先类的成员,避免了命名冲突。

优点:

  • 消除了菱形继承带来的二义性,以及数据冗余
  • 提高了代码的可维护性和可读性。

2)虚拟继承的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

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;
};

这个继承方式是这样的:A里有_a,B里有_b,C、D同理:
在这里插入图片描述

对于它内部内存的管理:

先来进行初始化:

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

对于普通菱形继承,不使用virtual关键字是这样的:

可以看到,它没有什么不同,就是按照顺序连续存储的,有两个A就造成了数据冗余与二义性。
在这里插入图片描述


对于带virtual关键字的菱形继承:

在这里插入图片描述

首先,我们可以发现,A只有一份了,然后这个公有的A被放到了最下面,这样就解决了二义性的问题

其次,在B和C中,多了一串奇奇怪怪的东西,如下图所示:
在这里插入图片描述
我们分别进入这两个地址,0x002B7B480x002B7B53,如下如所示:
在这里插入图片描述

这个地方红色框框出来的实际上就分别是B到A与C到A的偏移量!
我们叫它虚基表!!!

在这里插入图片描述

虚基表中红框部分存了偏移量,第一行是预留的,目前第二行是有效的。使用白框中的地址就可以找到偏移量,最终可以定位到A类中去!


4. 为什么要有虚基表?

为什么要有一个虚基表呢?下面这里白框的部分难道不能直接存A的地址吗?

在这里插入图片描述

原因有一下两个场景:

场景一:我们这里共同的只有一个A类,因此对于这里来说看不出差别,但是假设我还有其他的值要存呢?假设我还有EFG…要存在这里呢?
因此我们引入了虚基表,这些偏移量全部存到一个虚基表里边去,子类对象里只存虚基表的地址,利用偏移量来寻找所需的A。

场景二:我们这里只定义了一个d对象,假设我还有一个d1呢?这两个对象是一模一样的结构,它们相对偏移量的关系也是相等的,有了虚基表就可以传同一份虚基表的地址,通过相同的偏移量来找到A。
如下图:我们可以看到d与d1虚基表的地址是一样的!!!
在这里插入图片描述
在这里插入图片描述


5. 为什么要有偏移量?

那为什么又需要偏移量来找呢?
请问下面这段代码需不需要用到偏移量?

D d;
d._a = 1;

答案:不需要。
作为虚拟继承的它,编译器直到它的A在最下面,找到时候就直接去最下面找就可以了。

那什么时候会用到偏移量呢?
下面我也给出两个场景:

场景一:切片。假如有这样一段代码。
D d;
B b = d;
那么这个b作为父类就要去找d中相应的部分进行切片,但是d中是这样存的:
在这里插入图片描述
B的部分除了最上面蓝色的框还有最下面的A,因此找A就需要进行偏移量来找到。

场景2:

假设有这样一串代码:

D d;
d._a = 1;B b;
b._a = 2;
b._b = 3;B* ptr = &b;
ptr->_a++;ptr = &d;
ptr->_a++;

首先我们要知道,作为虚拟继承,不只是D的模型,连B的模型结构都变了,他变得与C保持一致。
如图:
在这里插入图片描述
b的模型已经不再是纯粹的,他也有虚基表,它的A也在最下面。

那么就会引发出一个问题,假设有这样的代码:
在这里插入图片描述
单看这里两个蓝色框里的代码,从表面看没有任何差异,对于编译器来说他并不知道实在调用b还是在调用d,因此只要我们取出偏移量,就可以根据偏移量来计算找到A。

我们可以来看一下汇编,这里是一模一样的,唯独偏移量不同:
在这里插入图片描述

因此,虚基表和偏移量都是必须的!!!

6. 关于解决数据冗余

但从下面这张图来说,好像没有解决数据冗余的问题。
在这里插入图片描述

但是,假设 _a是个数组呢?_a[10086],那么普通继承会继承很多份_a[10086],但是虚拟继承只继承一份,所以还是解决了数据冗余的问题。


三、小试牛刀

  1. 请问p1, p2, p3的关系是什么?
class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

分析如图:
在这里插入图片描述
谁先继承谁在上面L:
p1与p3所指向位置是一样的,但是p1与p3含义不同,p2在它们下面。
因此p1 = p3 != p2


  1. 请问下面代码打印顺序是什么?
#include<iostream>
using namespace std;
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;
}

答案是:在这里插入图片描述

这里考了两个点:

  • 虚拟继承只继承一份
  • 初始化列表顺序与构造顺序无关,谁先声明谁先构造。

四、库里的菱形继承

其实我们iostream就是一种菱形继承,库里的大佬驾驭得住,我们在实战中还是要尽量避免使用。
在这里插入图片描述


❤️继承的总结和反思

组合与继承的关系
在这里插入图片描述

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

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

  3. 继承和组合

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

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

    • 优先使用对象组合,而不是类继承。

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

    • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

    • 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

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


到这里就结束啦,创作不易,佬们三连支持一波🤩🤩🤩🥰🥰🥰<( ̄︶ ̄)↗[GO!]
在这里插入图片描述

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

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

相关文章

bootloader跳转app卡死(IAP卡死)

1、 关闭所有中断再跳转APP 一般bootloader跳转到APP时要关闭app中用到的中断(防止中断打断程序的运行&#xff0c;导致程序跑飞&#xff09;&#xff0c;那么查看系统中用到的中断&#xff1a;串口中断、滴答定时器中断&#xff0c;所以&#xff0c;跳转之前要关闭这两个中断&…

Vlan和Trunk

VLAN的定义 虚拟局域网&#xff0c;用来在二层网络中隔离广播域不同VLAN的设备在二层网络中无法互相通讯&#xff08;二层隔离技术&#xff09; VLAN的转发过程举例 源MAC字段后加上VLAN TAG字段&#xff0c;其中VLAN ID用来标识VLAN。 PC发送数据帧进入交换机&#xff0c;会…

使用SearXNG-搭建个人搜索引擎(附国内可用Docker镜像源)

介绍 SearXNG是聚合了七十多种搜索服务的开源搜索工具。我们可以匿名浏览页面&#xff0c;不会被记录和追踪。作为开发者&#xff0c;SearXNG也提供了清晰的API接口以及完整的开发文档。 部署 我们可以很方便地使用Docker和Docker compose部署SearXNG。下面给出Docker部署Se…

vscode插件live server无法在手机预览调试H5网页

环境 Window10、vscode&#xff1a;1.94.2、Live Server&#xff1a;v5.7.9、Live Server (Five Server)&#xff1a;v0.3.1 问题 PC端预览没有问题&#xff0c;但是在手机点击链接显示访问失败 排查 1. 是否同一局域网 意思就是电脑、手机是不是访问同一个网络。电脑插得…

微信互助学习平台(lw+演示+源码+运行)

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了微信互助学习平台的开发全过程。通过分析微信互助学习平台管理的不足&#xff0c;创建了一个计算机管理微信互助学习平台的方案。文章介绍了微信互助学习平台的…

论文精读:TiC-CLIP: Continual Training of CLIP Models(一)

论文精读&#xff1a;TiC-CLIP: Continual Training of CLIP Models&#xff08;一) 论文介绍 在多模态学习领域&#xff0c;CLIP&#xff08;Contrastive Language-Image Pre-training&#xff09;模型因其在图像和文本联合嵌入方面的卓越性能而受到广泛关注。然而&#xff0…

【C++】vector(1)

&#x1f608;个人主页: 起名字真南 &#x1f608;个人专栏:【数据结构初阶】 【C语言】 【C】 目录 引言1 vector 的基本知识1.1 vector 的特点 2 vector 的主要功能和操作2.1 vector 的构造2.2 vector 的增删改查2.3 vector 的容量 引言 在C的标准模板库&#xff08;STL&…

MySQL——数据库

什么是数据库 数据库&#xff08;DB , DataBase&#xff09;概念&#xff1a;数据仓库&#xff0c;软件&#xff0c;安装在操作系统&#xff08;window&#xff0c;linux&#xff0c;max&#xff0c;...&#xff09;之上学习数据库最重要的就是学习SQL语句存储500万以下的数据…

鸿蒙HarmonyOS————ArkTs介绍(1)

最近除了人工智能&#xff0c;还有一个很火的HarmonyOS&#xff0c;HarmonyOS是华为公司开发的一款面向全场景的分布式操作系统&#xff0c;旨在为消费者提供跨设备无缝协同体验。它支持多种智能终端设备&#xff0c;包括但不限于智能手机、平板电脑、智能穿戴设备、智能家居设…

<Project-11 Calculator> 计算器 0.3 年龄计算器 age Calculator HTML JS

灵感 给工人发工资是按小时计算的&#xff0c;每次都要上网&#xff0c;我比较喜欢用 Hours Calculator &#xff0c;也喜欢它的其它的功能&#xff0c; 做个类似的。 我以为是 Python&#xff0c;结果在学 javascript 看 HTML&#xff0c;页面的基础还停留在 Frontpage 2000…

【学术论文投稿】自动化运维:解锁高效运维的密钥

【连续三届IEEE出版|EI检索】第三届图像处理、计算机视觉与机器学习国际学术会议&#xff08;ICICML 2024&#xff09;_艾思科蓝_学术一站式服务平台 更多学术会议请看&#xff1a;https://ais.cn/u/nuyAF3 目录 引言 一、自动化运维概述 1. 自动化运维的定义 2. 自动化运…

Qt中使用线程之QRunnable

1、自定义1个子类继承自QRunnable 2、重写run方法&#xff0c;编写子线程的业务逻辑 3、使用QThreadPool的全局方法来开启这个线程 4、线程的回收不需要关注&#xff0c;由QThreadPool处理 5、缺点&#xff1a;无法使用信号槽机制 6、适合一些不需要和主线程通信的耗时的任…

SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载

文章目录 前言一、控制器层代码二、服务层代码三、代码亮点分析 前言 SpringBoot的同步excel导出方式中&#xff0c;服务会阻塞直到Excel文件生成完毕&#xff0c;如果导出数据很多时&#xff0c;效率低体验差。有效的方案是将导出数据拆分后利用CompletableFuture&#xff0c;…

【图论】(四)最小生成树与拓扑排序

最小生成树与拓扑排序 最小生成树之prim&#xff08;P算法&#xff09;相关概念结题思路拓展 最小生成树之kruska&#xff08;K算法&#xff09;过程模拟程序实现拓展 拓扑排序背景与思路模拟过程程序实现 最小生成树之prim&#xff08;P算法&#xff09; 相关概念 P算法是用…

『完整代码』坐骑召唤

创建一个按钮 作为召唤/消失坐骑的开关 将预制体放入指定文件夹 命名为Mount01 创建脚本并编写&#xff1a;CallMount.cs using UnityEngine; using UnityEngine.UI; public class CallMount : MonoBehaviour{public Button callBtn;GameObject mountPrefab;GameObject mountIn…

CentOS7 上安装GitLab的经历

一、安装必要的基础环境 1.安装依赖包 [rootgitlab-server ~]#yum install curl policycoreutils openssh-server openssh-clients postfix wget git patch -y [rootgitlab-server ~]# systemctl start postfix 2.配置yum源(由于网络问题&#xff0c;国内用户请使用清华大学…

导数的概念及在模型算法中的应用

一. 导数概念与计算 1. 导数的物理意义&#xff1a; 瞬时速率。一般的&#xff0c;函数yf(x)在x处的瞬时变化率是 2. 导数的几何意义&#xff1a; 曲线的切线&#xff0c;当点趋近于P时&#xff0c;直线 PT 与曲线相切。容易知道&#xff0c;割线的斜率是当点趋近于 P 时&…

数字孪生:引领智慧农业的未来

在现代农业中&#xff0c;数字化与智能化的浪潮正在改变传统的种植方式。数字孪生技术作为一种创新的数字化解决方案&#xff0c;正在深刻改变智慧农业的面貌&#xff0c;尤其是在大棚智能控制、数据全面可视、加工过程监控和物流运输溯源等方面展现出巨大的潜力。 frontop数字…

【动手学深度学习】8.1. 序列模型(个人向笔记)

想象一下有人正在看网飞&#xff08;Netflix&#xff0c;一个国外的视频网站&#xff09;上的电影。 一名忠实的用户会对每一部电影都给出评价&#xff0c; 毕竟一部好电影需要更多的支持和认可。 然而事实证明&#xff0c;事情并不那么简单。 随着时间的推移&#xff0c;人们对…

《Python基础教程》笔记(ch0-1)

前言 在Python生态系统中&#xff0c;各种包轮番登场&#xff0c;各种编码实践大行其道后又日渐式微。 引言 Python是什么&#xff1f;为何要使用它&#xff1f;官方宣传说&#xff1a;Python是一种面向对象的解释性高级编程语言&#xff0c;具有动态语义。 这句话的要点在…