【C++】继承(菱形继承的深入理解)

在本篇博客中,作者将会带领你深入的理解C++中的继承。

注意!!!本篇博客是在32位机器下进行讲解的,64位下会有所不同,但大同小异。

一. 继承的概念及定义

继承的概念 

什么是继承?为什么要有继承?

继承是一个类继承另外一个类的所有成员,那么为什么要有继承呢?

继承是面向对象设计中提高代码复用的重要手段,它允许我们在原有类的基础上进行扩展增加功能,这样就能产生新的类,也叫做子类或者派生类,被继承的叫做父类或者基类


如下代码就是一个继承的例子: 

 

在这个代码中,Student继承了Person所有成员,从监视窗口中也可以看到Student s1对象中有一个Person的数据,就是继承Person下来的。

继承的定义

那么继承又是如何编写的?

从上面的代码不难看出继承的编写方法:

从图中,子类和父类都很好理解,那么继承方式又是啥?

在这里,继承方式要与访问修饰符来一起看。

 

继承的方式一共有三种:public继承protected继承private继承。 

那么这三种继承方式又有什么区别呢?

区别如下表所示。

举例子就是说,一个子类通过继承后,原来父类中的成员的访问权限原本访问权限继承方式权限较小的那一个。

如下图所示。 

总结:

1.基类的private成员,不管以什么方式的继承,在子类中都不可见,不可见的意思时,其实在子类中存在private修饰的成员,只不过我们没权限去访问它。

2.基类private修饰的成员在类外是不能访问的,被继承后在派生类中也不能访问,但是如果我们想要一个成员不能被类外访问,但是继承后能在派生类中访问,这个时候我就要使用protected来修饰成员,所以可以看出protected是为了继承才出现的。

3.使用class时默认的继承方式private,使用struct时默认的继承方式public

4.实际中,public继承方式最常用,protected和private继承方式很少见。  

二.基类和派生类中的赋值转换 

派生类对象可以直接给基类对象/指针/引用赋值,这种行为也称作切片

但是反过来,基类不能给派生类赋值。

切片

三.继承中的作用域 

1.在继承体系中基类派生类都有独立的作用域

2.当基类和派生类中有同名的成员变量同名的成员函数时,子类的成员将会屏蔽掉父类的成员,这种情况叫做隐藏也叫重定义,但是如果你也想在子类中访问屏蔽掉的父类成员,也可以通过        基类::成员        这种形式去访问。

3.成员函数当函数名相同时就构成隐藏

成员变量隐藏 

以下这种情况就是 成员变量构成隐藏。

#include<iostream>
using namespace std;

class Person
{
public:

	int _age = 1;
	int _num = 2;
};

class Student :public Person
{
public:

	int _num = 10;
};

int main()
{
	Person p1;
	Student s1;

	cout << s1._num << endl;//默认情况直接输出_num是子类的_num
	cout << s1.Person::_num << endl;//如果想要输出基类的_num,就要指定Person的作用域

	return 0;
}

成员函数隐藏

下面这种情况,在基类和派生类中,有同名的函数,所以构成隐藏注意:不是函数重载) 

四.派生类的默认成员函数

在C++中,我们知道类是有默认成员函数的,那么在派生类中,它的默认成员函数又是怎么样的呢?下面我们来探讨一下。

构造函数

我们先来说结论:

①派生类的构造函数必须调用基类的构造函数完成基类成员的初始化

 

②当基类没有默认构造函数时(默认构造函数是不需要传参就能调用的构造函数),则必须在派生类的初始化列表显式调用

析构函数

从构造函数的部分可以看出,派生类构造时,要先去调用基类的构造,再去调用派生类的构造,那么析构函数是不是也如此?是的。 

 

从图中,我们可以看出,当s1的生命周期结束时,会去调用~Student这个析构函数,当这个析构函数调用完成后,还会再去调用基类的析构函数。 

拷贝构造

同样的,拷贝构造也是一个道理,派生类调用拷贝构造时,要先去调用父类的拷贝构造来完成基类成员的拷贝,再去调用派生类的拷贝构造。 

赋值=重载

同样的,赋值=重载,也是一个道理,派生类的赋值=重载要先去调用基类的赋值=重载再调用自身的赋值=重载。 

 五.继承与友元

结论:友元关系不能继承,也就是说,基类的友元函数不能访问派生类的成员。 

六.继承与静态成员

结论:一个在基类中定义的静态成员,被基类和派生类共享,也就是说,在所有基类和派生类对象中,它们共用一个静态成员。 

七.菱形继承以及菱形虚拟继承

菱形继承是一个复杂问题,因为菱形继承会带来二义性数据冗余的问题,所以为了解决这两个问题, 才有了菱形虚拟继承

菱形继承 

首先什么是菱形继承?在探讨菱形继承之前,我们先来看看继承有哪几种。

单继承多继承菱形继承。每次继承如下图所示。 

其中,菱形继承是多继承的一种特殊形式。那么菱形继承会带来什么问题呢?我们举一个例子来看看。就如上图中的菱形继承:

我先定义一个Person类,再派生两个Student、Teacher类,再通过这两个类,派生出Assiant类。

#include<iostream>
using namespace std;

class Person
{
public:
	int _age;//年龄
};

class Student :public Person
{
public:
	int _sno;//学生学号
};

class Teacher :public Person
{
public:
	int _tno;//教师编号
};

class Assiant :public Student, public Teacher
{
public:
	int _aaa;
};

int main()
{
	Assiant a1;
	return 0;
}

此时Assiant类就是一个菱形继承的情况,那么它实例出来的对象是怎样的呢?

通过监视窗口以及画图来看,可以发现在Assiant实例出的对象中,有两份_age,这两份的_age是分别来自Student类继承Person而来,以及Teacher类继承Person而来的。 

 

那么菱形继承会带来什么问题呢?菱形继承会带来二义性以及数据冗余的问题。我们继续往下看。 

二义性 

那么二义性到底是什么呢?这个很好理解,就是在Assiant实例的对象中,有两个_age,当我们给_age赋值时,就会出现不知道指的是那个_age。

如下图所示: 

 

当我们尝试给_age赋值的时候,会报错,显示_age不明确,那是因为在a1中,有两个_age,一个是继承Student来的,一个是继承Teacher来的,那么该如何解决呢?很简单,只需要指定是那个父类的_age即可(因为Assiant是继承两个父类而来的)。 

数据冗余

通过指定那个父类的成员来赋值解决了二义性的问题,但是数据冗余没有解决,一个Assiant对象里面有两个_age,但我们只需要一个即可,那么这个时候又该怎么办呢?所以这个时候需要用到虚拟继承。虚拟继承就是为了解决菱形继承问题而出现的。 现在,我们给Student和Teacher类加上虚拟继承再来看看。

#include<iostream>
using namespace std;

class Person
{
public:
	int _age;//年龄
};

class Student :virtual public Person//使用虚拟继承
{
public:
	int _sno;//学生学号
};

class Teacher :virtual public Person//使用虚拟继承
{
public:
	int _tno;//教师编号
};

class Assiant :public Student, public Teacher
{
public:
	int _aaa;
};

int main()
{
	Assiant a1;
	a1._age = 1;//数据冗余问题得到了解决,可以直接给_age赋值
	return 0;
}

通过使用虚拟继承的方式,在Assiant对象中,数据的二义性以及数据冗余的问题都得到了解决。 那么虚拟继承是如何解决菱形继承带来的问题的呢,这个时候,我们在全盘来看一下。

虚拟继承是如何解决菱形继承所带来的问题

在解释虚拟继承是如何解决菱形继承带来的问题前,我们先来看一下,普通菱形继承的底层对象模型是怎样的。 我们来使用vs的内存窗口来查看。

普通菱形继承的底层对象模型 

#include<iostream>
using namespace std;

class Person
{
public:
	int _age;//年龄
};

class Student :public Person
{
public:
	int _sno;//学生学号
};

class Teacher :public Person
{
public:
	int _tno;//教师编号
};

class Assiant :public Student, public Teacher
{
public:
	int _aaa;
};

int main()
{
	Assiant a1;
	a1.Student::_age = 1;
	a1.Teacher::_age = 2;
	a1._sno = 3;
	a1._tno = 4;
	a1._aaa = 5;
	return 0;
}

通过运行上面的代码,我们通过调试和内存窗口来观察。 

发现,普通菱形继承中,它的底层对象模型的布局和我们上面画的图一样。

这就是为什么普通的菱形继承会带来二义性以及数据冗余的问题。

虚拟菱形继承的底层对象模型

看完了普通继承的底层对象模型,接下来我们再来看看虚拟菱形继承的底层对象模型,看看它是如何解决二义性以及数据冗余问题的。 

#include<iostream>
using namespace std;

class Person
{
public:
	int _age;//年龄
};

class Student :virtual public Person//使用虚拟继承
{
public:
	int _sno;//学生学号
};

class Teacher : virtual public Person//使用虚拟继承
{
public:
	int _tno;//教师编号
};

class Assiant :public Student, public Teacher
{
public:
	int _aaa;
};

int main()
{
	Assiant a1;
	a1._age = 1;
	a1._sno = 3;
	a1._tno = 4;
	a1._aaa = 5;
	return 0;
}

通过观察,我们发现,原来存_age的位置变到了最底下去了,而且只有一个,这样就解决了二义性的问题,这个很好理解,但是数据冗余呢?

有的同学可能会问,那么两个问号(?)的地方又存的啥,是浪费了还是怎么样了,而且这样看上去好像数据还多余了。

 

观察力较强的同学可能已经发现了,两个问号(?)的位置似乎是一个指针,答案也没错,两个问号(?)的地方确实是一个指针,那么这个指针是什么指针,又指向什么内容?

这两个指针叫做虚基表指针,它们各自指向一张虚基表,那么虚基表里面又是啥,我继续通过内存窗口来进行查看。

 通过内存窗口我们可以看到,两个指针指向的内容中,又有一个值,分别是2012,那么这两个20和12又有什么含义呢,观察力强的人可能会发现,20是第一个指针距离_age变量的偏移量,12是第二个指针距离_age变量的偏移量


 看到这里,我们基本理解了虚拟菱形继承的底层对象模型是怎样的了,也懂了虚拟继承是如何解决二义性和数据冗余的问题的。

但是可能会有同学问:不对啊,在没有使用虚拟继承时,一个对象只有五个变量,使用虚拟继承后,一个对象又六个变量,因为虽然_age少了一个,但是多了两个指针。如下图所示。

 

你说的没错,在这种情况下,一个对象确实变大了,但是是否造成数据冗余不能这么看,假设我们将Person类中_age换成一个数组,再通过求普通菱形继承虚拟菱形继承的大小来看看。

我们发现,普通菱形继承的大小比虚拟菱形继承的大小大得多,那是因为对于虚拟菱形继承的底层对象模型来说,少了一个int _age[10],只多了两个指针,所以虚拟菱形继承小的多,这个时候就体现出了虚拟菱形继承为什么能解决数据冗余的问题了。


当然这个时候,可能会有同学又会有问题,为什么虚基表里面要存偏移量呢?我们不能直接访问到_age成员吗?

 当我们定义一个Student或者Teacher对象变量的时候,对象中的成员变量都是顺着来存储的,当我们想要访问对象的时候,也可以顺着来读而获取该值,这个很好理解,但是用了虚拟菱形继承后,Student对象和Teacher对象的_age不再是顺着来存储而是变到了最后的位置,那么如果我们想要读取Student的_age成员时,因为要顺着来读取,当我们读取到虚基表指针时,就可以通过虚基表中的偏移量来找到_age的位置,否则将会找不到。

 

八.总结 

多继承是C++的一个缺陷之一,因为有了多继承就会有菱形继承,有了菱形继承就有了菱形虚拟继承,非常复杂,因为要解决二义性以及数据冗余的问题,但是不要慌,在实际写代码中,菱形继承是很少用的,一般来说不建议使用多继承,更不要使用菱形继承,对于菱形继承来说,理解到它的底层对象模型实现就差不多了。

继承和组合 

继承大家应该都懂,那么组合又是什么,最开始我提到,继承可以提高代码的复用性,因为它允许我们在基类的基础上进行扩展。那么组合呢?我接下来举个例子来看看。

class Person
{
public:
	int _age;
};

class Student
{
public:
	Person p1;
	int _sno;
};

在最上面的继承中,为了能复用Person类,我们使用Student类去继承Person,这样就可以达到复用Person类的成员,那么其实不用继承也可以实现,我们也可以用组合来实现如上面的代码一样,我们在Student类中组合一个Person的对象,也能达到我们的目的,那么实际写代码中,继承和组合我们应该如何选择呢?


1.继承是一种is-a的关系,即它表示一个类是另一个类的特殊形式,也可以说一个派生类就是一个基类,怎么理解呢?例如,上面的Person和Student中,Person是一个人,Student也是一个人,所以一般来说使用继承来实现。

2.组合是一种has-a的关系,即一个类中包含了另一个类,例如,车和轮子的关系,车中包含了轮子,所以我们在设计一个车和轮子类的时候,可在车类中组合轮子。

3.继承是一种“白箱”复用,什么是“白箱”,即它是一个透明的内部是可见的,在子类中,父类的内部细节对于子类来说是可见的,但是继承在一定程度上破坏了封装,因为父类的protected成员在子类中是可见的,同时继承对于子类和父类来说,它们之间的关系是很强的,耦合度很高

4.组合是一种“黑箱”复用,即在组合中,一个类对象的成员变量,对于这个组合类来说,它的内部细节是不可见的。类与类直接的关系很弱耦合度低

5.一般来说,优先使用组合,而不是继承,因为当组合和继承都能用的时候,组合的复杂性对于继承来说肯定比较低,当两个都能用的时候,我们肯定是希望用最简单的。而且组合的耦合度低,代码维护性更好,而继承的耦合度高父类的变化会影响子类,但是继承也不是说不用,正如前面所说的,继承是is-a的关系,组合是has-a的关系,我通过这两个关系来决定就好了。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日OJ题_贪心算法四⑤_力扣354. 俄罗斯套娃信封问题

目录 力扣354. 俄罗斯套娃信封问题 解析代码1_动态规划&#xff08;超时&#xff09; 解析代码2_重写排序贪心二分 力扣354. 俄罗斯套娃信封问题 354. 俄罗斯套娃信封问题 难度 困难 给你一个二维整数数组 envelopes &#xff0c;其中 envelopes[i] [wi, hi] &#xff0…

AI代理和AgentOps生态系统的剖析

1、AI代理的构成&#xff1a;AI代理能够根据用户的一般性指令自行做出决策和采取行动。 主要包含四个部分&#xff1a; &#xff08;1&#xff09;大模型&#xff08;LLM&#xff09; &#xff08;2&#xff09;工具&#xff1a;如网络搜索、代码执行等 &#xff08;3&#x…

基于截断傅里叶级数展开的抖动波形生成

1、背景 抖动是影响信号完整性的重要因素。随着信号速率的不断提高&#xff0c;抖动的影响日益显著。仿真生成抖动时钟或抖动信号&#xff0c;对系统极限性能验证具有重要意义。抖动是定义在时域上的概念&#xff0c;它表征真实跳变位置(如跳边沿或过零点)与理想跳变位…

事务-MYSQL

目录 1.事务操作演示 2.事务四大特性ACID 3.并发事务问题 4. 并发事务演示及隔离级别​编辑​编辑​编辑​编辑​编辑​编辑​编辑 1.事务操作演示 默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。 方式二 2.事务四大特性ACID 原子…

数据结构与算法===贪心算法

文章目录 定义适用场景柠檬水找零3.代码 小结 定义 还是先看下定义吧&#xff0c;如下&#xff1a; 贪心算法是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法。 适用场景 由于…

GDPU JavaWeb 过滤器

再纯净的白开水也过滤不了渣茶。 Servlet登陆页面 引入数据库&#xff0c;创建用户表&#xff0c;包括用户名和密码&#xff1a;客户端通过login.jsp发出登录请求&#xff0c;请求提交到loginServlet处理。如果用户名和密码跟用户表匹配则视为登录成功&#xff0c;跳转到loginS…

【harbor】harbor的搭建与使用

harbor的搭建与使用 文章目录 harbor的搭建与使用1. harbor的下载2. 创建ssl证书3.harbor的配置3. docker修改4.启动harbor5.使用docker总结 1. harbor的下载 harbor仓库地址&#xff1a;https://github.com/goharbor/harbor harbor主要是go语言写的&#xff0c;但是我们dock…

MySQL相关文件的介绍

其中的pid-file/var/run/mysqld/mysqld.pid是用来定义MySQL的进程ID的信息的&#xff0c; 这个ID是操作系统分配给MySQL服务进程的唯一标识&#xff0c;使得系统管理员可以轻松识别和管理该进程。 其中的log-error/var/log/mysqld.log是MySQL的错误日志文件&#xff0c;如果有…

ssm120基于SSM框架的金鱼销售平台的开发和实现+jsp

金鱼销售平台 摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于金鱼销售平台当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了金鱼销售平台&#xff0c;它彻底改…

VP Codeforces Round 944 (Div 4)

感受&#xff1a; A~G 其实都不难&#xff0c;都可以试着补起来。 H看到矩阵就放弃了。 A题&#xff1a; 思路&#xff1a; 打开编译器 代码&#xff1a; #include <iostream> #include <vector> #include <algorithm> #define int long long using na…

探索互联网医院系统源码:开发在线药房小程序实战教学

今天&#xff0c;笔者将与大家一同深入探讨互联网医院系统的源码结构&#xff0c;并通过开发在线药房小程序的实战教学&#xff0c;为读者提供一种学习和理解这一领域的途径。 一、互联网医院系统源码解析 1.技术选型 互联网医院系统的开发离不开合适的技术选型&#xff0c;…

python 前台页面和oracle数据库联动案例-用户注册

今天是python 入门day3&#xff0c;案例实现界面如图&#xff0c;比较简单&#xff0c; 一个简单的用户注册页面&#xff0c;并且可以与Oracle数据库进行交互。 界面如图&#xff1a; 实现这个简单demo的过程中&#xff0c;遇到很多错误&#xff0c; 1、提交过程中提示主键不…

关于我转生从零开始学C++这件事:获得神器

❀❀❀ 文章由不准备秃的大伟原创 ❀❀❀ ♪♪♪ 若有转载&#xff0c;请联系博主哦~ ♪♪♪ ❤❤❤ 致力学好编程的宝藏博主&#xff0c;代码兴国&#xff01;❤❤❤ 几天不见 &#xff0c;甚是想念&#xff01;哈咯大家好又是我大伟&#xff0c;五一的假期已经结束&#xff0…

【AMBA Bus ACE 总线 7.1 -- ACE Domains 介绍 2】

请阅读【AMBA Bus ACE 总线与Cache 专栏 】 欢迎学习:【嵌入式开发学习必备专栏】 文章目录 AxDOMAINAxDOMAIN[1:0]的值及含义AxDOMAIN 在ARM的AXI Coherency Extensions (ACE) 协议中,AxDOMAIN[1:0]是一个重要的信号字段,用于指示传输的域类型。这个字段影响了传输对系统…

【Element-UI快速入门】

文章目录 **Element-UI快速入门****一、Element-UI简介****二、安装Element-UI****三、引入Element-UI****四、使用Element-UI组件****五、自定义Element-UI组件样式****六、Element-UI布局组件****七、Element-UI表单组件****八、插槽&#xff08;Slots&#xff09;和主题定制…

vue+springboot项目服务器部署

①创建一台opencloud8的腾讯云服务器 ②用xshell连接服务器 ③vue中新建.env.development配置文件 .env.development: VUE_APP_BASEURLhttp://localhost:9090 .env.production: VUE_APP_BASEURLhttp://服务器ip:9090 ④修改main.js import Vue from vue import App from ./A…

【LAMMPS学习】八、基础知识(6.3)使用 LAMMPS GUI

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语,以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各种模拟。 …

【Threejs进阶教程-算法篇】1.常用坐标系介绍与2d/3d随机点位算法

2d/3d随机算法 学习ThreeJS的捷径坐标系简介平面直角坐标系和极坐标系空间直角坐标系圆柱坐标系球坐标系球坐标系与直角坐标系的转换 基于坐标系系统的随机点位算法平面直角坐标系随机平面直角坐标系随机的变形 空间直角坐标系随机二维极坐标系随机圆柱坐标系随机基于Cylinderc…

MathType永久激活版写毕业论文必备神器以及破解版下载图文教程(附mathtype7镶嵌到word步骤)

前言 由于临近暑假&#xff0c;大学生和研究生都需要写自己的论文。使用的工具叫做MathType&#xff0c;它是加拿大的公司开发的&#xff0c;今天给大家带来的是Win和Mac版Mathtype最新破解版。 自从Mathtype7的发布&#xff0c;很多的老师和学生都不知道它从哪里下载和激活&…

Web前端一套全部清晰 ⑧ day5 CSS.3 选择器、PxCook软件、盒子模型

谁不是一路荆棘而过呢 —— 24.5.12 CSS.3 选择器、PxCook软件、盒子模型 一、选择器 1.结构伪类选择器 1.作用: 根据元素的结构关系查找元素。 选择器 说明 E:first-child 查找第一个 E元素 E:last-child 查找最后一个E元素 E:nth-chil…