【C++】C++面向对象编程三大特性之一——继承

❤️前言

        本篇博客主要是关于C++面向对象编程中的三大特性之一的继承,希望大家能和我一起共同学习进步!

正文

        我们刚刚学习一块全新的知识,首先简单关注一下它的概念和简单的使用方法。

继承的概念及定义

继承的概念

        继承的概念:继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

        继承是类设计层次的复用,让我们在一个类的基础上拓展出许多不同的新类,它们之间的关系大概是新类即包含原类的特性,也有自己独特的特性,例如狗类可以拓展出卷毛狗类、直毛狗类等派生类。这样的拓展就是继承,对于派生类来说,我们就是复用了它的基础类的代码。

继承的定义

        了解了继承的概念之后,我们继续来看继承的定义方式,下面以人类和学生类做演示:

//     派生类   继承方式  基类
class Student : public  Person
{
public:int _stuid; // 学号int _major; // 专业
};

        上面的代码就是继承的定义方式,其中的人类Person被称作基类或者父类,学生类被称作派生类或者子类。

        继承方式和我们的访问限定符共用关键字,public 是公有继承,protected 是保护继承,private 是私有继承。

继承基类成员访问方式的变化

        当子类使用不同的继承方式继承了基类的成员之后,子类访问父类成员的情况(也就是父类成员在子类这里的访问权限相当于子类的哪种成员)如下表:

        我们在学校学习继承相关知识的时候,老师一定会给我们一个类似的表让我们去记,但是其实我们一般只需要记住 public 继承即可,这是因为 private 继承和 protected 继承在现实中几乎不太会使用。而且由于 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强,也不太推荐使用它们。

        这里有一个偏门的小知识点:其实在定义继承时,使用关键字class定义类默认的继承方式private,使用struct时默认的继承方式是public,但是我们可以看到平时我们都是显式写出继承方式的,默认的方式并不被推荐。

基类和派生类对象之间赋值转换

        通过上面的一些知识的学习,我们可以发现:当我们定义一个继承关系之后,如果我们有一个子类对象,其实可以将父类的内容看做是子类对象的一部分,那么我们就可以看看父子类之间是否能进行赋值转换。

        在尝试之后我们可以发现,派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。大概情景如下:

        子类可以赋值给父类,但是父类却不能赋值给子类,想想也十分的合理,父类中并没有子类特有的某些信息,那么凭什么赋值给子类呢?

        测试代码如下:

class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:int _No; // 学号
};int main()
{Student sobj;// 1.子类对象可以赋值给父类对象 / 指针 / 引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.基类对象不能赋值给派生类对象// 错误 sobj = pobj;return 0;
}

继承中的作用域

        现在我们继续来看继承中的作用域相关知识:

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用基类::基类成员显式访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

测试代码:

class Person
{
public:void func(int x){cout << "P::func" << endl;}
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:void func(){cout << "S::func" << endl;}int _No; // 学号
};int main()
{Student stu;stu.func();return 0;
}

派生类的默认成员函数

        派生类继承了基类的特征,那么在派生类中的默认成员函数与普通的类会有什么不同呢?

        派生类的默认成员函数需要我们记住的点大概有这些:

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=()必须要调用基类的operator=()完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会学到)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

        关于派生类和基类的构造和析构函数,它们的调用情况大概如下图:

        这里构造和析构的顺序对应着栈空间的先进后出,我们可以根据这个进行记忆,除此之外,还有一些原因就是派生类的一些构造析构行为可能会用到基类的数据。

继承与友元

        友元在继承中的特点就是:友元关系不能被继承,形象的比喻就是:父亲的朋友一开始不一定是我的朋友,他必须自己和我们确认朋友关系才能成为朋友。

继承与静态成员

        基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

        这也可以做比喻,人数是人类的静态成员,小孩是人类的子类,小孩增加了难道人数会为这个小孩重新计数吗?小孩也是人类中的一种,具有人类共同的一些特征,这些特征可以对应静态成员。

        也可以说,派生类只继承了静态成员的使用权,但是并不会增加静态成员的实例。

测试代码:

class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};
int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_count << endl;Student::_count = 0;cout << " 人数 :" << Person::_count << endl;
}int main()
{TestPerson();return 0;
}

 复杂的菱形继承和菱形虚拟继承

        继承的种类可以由派生类继承基类的数量分为单继承和多继承。

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

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

        多继承的定义只需要在原有的单继承关系之后加上逗号和新的继承关系即可。

        而当我们使用多继承的时候,就会引出一个比较复杂的问题,也就是菱形继承问题:

        菱形继承似乎在现实中确实会有应用场景,但是它会引发一些问题,当我们使用如上的菱形继承关系,那么在Assistant类中似乎就会存在两个Person类,也就是说他可能具有两份人的特征,这会造成数据冗余和二义性的问题,更详细的我们可以通过如下的对象成员模型进行分析:

        那我们是否有方法去解决这样的问题呢?这时候我们就可以使用虚拟继承,虚拟继承可以很好地解决菱形继承所带来的数据冗余和二义性的问题,但需要注意的是,虚拟继承也只在菱形继承中有用,其他地方不建议使用。

        上述关系的菱形虚拟继承的代码如下:

class Person
{
public:string _name; // 姓名
};// 要使用菱形虚拟继承需要在中间位置的继承方式前加上 virtual 关键字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; // 主修课程
};void Test()
{Assistant a;Teacher& t = a;Student& s = a;a._name = "Joker";t._name = "Peter";s._name = "张三";}int main()
{Test();return 0;
}

        调试这段代码,我们可以发现通过菱形虚拟继承,Assistant对象中最终只含有一个Person类的数据,成功的解决了数据冗余和二义性的问题。

        那么虚拟继承是如何解决这个问题的呢?我们现在来了解菱形虚拟继承的底层原理。

菱形虚拟继承的底层原理

        下面是菱形虚拟继承的测试代码,我们可以通过调试下面的代码来研究底层原理。

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

        我们调试上面的代码时需要结合d对象的内存来看,这样可以更清晰的得到对象的存储模型,具体的内存分布如下图:

        我们可以看见d对象的b部分和c部分除了他们本身的数据成员以外还多出了一份数据,那么这个数据代表着什么意思呢?我们可以看到这个数据得大小是四个字节,而且有点不规整,那么很有可能就是一个指针,也确实猜对了,这个数据就是一个指针。它的名字叫做虚基表指针,指向一个叫做虚基表的东西,那么现在让我们看看这两个指针指向的虚基表:

        这两个虚基表中分别存了两个有效数据从b部分到c部分为20和12,它们的意思分别为这两个部分到a部分的偏移量,单位为字节。通过这样的模型,我们就可以通过保存的偏移量的值和地址来控制公有数据,也就是a部分的数据。

        将上面的内存结构整理成如下的结构示意图:

继承的总结和反思

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

继承和组合

        继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
        因此我们尽量会选择另一种类设计层次复用的方式,也就是组合——对象组合是类继承之的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

        简单来说,继承关系就是is-a,而组合关系则是has-a,继承中子类是父类的一种,而组合则是一个类是另一个类的成员。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

🍀结语

        今天的博客就到此为止啦,希望能对大家有用。

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

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

相关文章

HDFS HA 高可用集群搭建详细图文教程

目录 一、高可用&#xff08;HA&#xff09;的背景知识 1.1 单点故障 1.2 如何解决单点故障 1.2.1 主备集群 1.2.2 Active、Standby 1.2.3 高可用 1.2.4 集群可用性评判标准&#xff08;x 个 9&#xff09; 1.3 HA 系统设计核心问题 1.3.1 脑裂问题 1.3.2 数据状…

从C++98到C++26,经历了什么?

作为一名程序员&#xff0c;2023年最应该关注的话题就是C的标准更新。 C标准遵循3年开发周期&#xff0c;并以发布年份命名。C在临发布的一年前会进入“功能冻结期&#xff08;feature freeze&#xff09;”&#xff0c;即版本开发进入半稳定状态&#xff0c;不再添加重大功能及…

【RPC 协议】序列化与反序列化 | lua-cjson | lua-protobuf

文章目录 RPC 协议gRPCJSON-RPC 数据序列化与反序列化lua-cjsonlua-protobuf RPC 协议 在分布式计算&#xff0c;远程过程调用&#xff08;英语&#xff1a;Remote Procedure Call&#xff0c;缩写为 RPC&#xff09;是一个计算机通信协议。该协议允许运行于一台计算机的程序调…

机器学习部分知识点总结

文章目录 基本概念N与NP泛化能力性能度量比较检验 线性回归逻辑回归神经网络 基本概念 N与NP P问题&#xff1a;一个问题可以在多项式&#xff08;O(n^k) 的时间复杂度内解决 例如&#xff1a;n个数的排序&#xff08;不超过O(n^2)&#xff09; NP问题&#xff1a;一个问题的解…

Leetcode 16.07 最大数值

编写一个方法&#xff0c;找出两个数字a和b中最大的那一个。不得使用if-else或其他比较运算符。 示例&#xff1a; 输入&#xff1a; a 1, b 2 输出&#xff1a; 2 我的答案&#xff1a; 为了找出两个数中的较大者&#xff0c;而不使用比较或条件语句&#xff0c;我们可以…

常见的几种排序算法

目录 一、插入排序 1、直接插入排序 1.1、排序方法 1.2、图解分析 1.3、代码实现 2、希尔排序 2.1、排序方法 2.2、图解分析 2.3、代码实现 二、选择排序 1、直接选择排序 1.1、排序方法 1.2、图解分析 1.3、代码实现 2、堆排序 2.1、排序方法 2.2、图解分析 …

视频监控/视频汇聚/视频云存储EasyCVR平台接入国标GB协议后出现断流情况,该如何解决?

视频监控汇聚平台EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。安防监控平台EasyCVR既具备传统安防视频监控的能…

优思学院|亲和图案例:寻找六西格玛的项目

什么是亲和图&#xff1f; 亲和图&#xff08;Affinity Diagram&#xff09;主要功能在於分类归纳&#xff0c;协助在一堆杂乱无章的资料之中&#xff0c;有系统的归纳出几个大类&#xff0c;以利后续作业。通常先利用头脑风暴&#xff08;Brainstorming&#xff09;方式得到大…

K8S:K8S自动化运维容器

目录 一.k8s概述 2.为什么要用K8S 3.作用及功能 4.k8s容器集群管理系统 二.K8S的特性 1.弹性伸缩 2.自我修复 3.服务发现和复制均衡 4.自动发布和回滚 5.集中化配置管理和秘钥管理 6.存储编排 7.任务批量处理运行 三.K8S的集群架构 四.K8S的核心组件 1.Master组件 …

命令执行漏洞复现攻击:识别威胁并加强安全

环境准备 这篇文章旨在用于网络安全学习&#xff0c;请勿进行任何非法行为&#xff0c;否则后果自负。 一、攻击相关介绍 原理 主要是输入验证不严格、代码逻辑错误、应用程序或系统中缺少安全机制等。攻击者可以通过构造特定的输入向应用程序或系统注入恶意代码&#xff…

计算机网络的故事——HTTP报文内的HTTP信息

HTTP报文内的HTTP信息 文章目录 HTTP报文内的HTTP信息一、HTTP 报文二、请求报文及响应报文的结构三、编码提升传输速率 一、HTTP 报文 HTTP报文是由多行&#xff08;CRLF作换行符&#xff09;数据构成的字符串文本&#xff0c;HTTP报文可以分为报文首部和报文主体两部分&…

数据结构(一)Trie字符串统计

目录 代码 &#xff08;一&#xff09;输入cat son[p][u],p表示儿子&#xff0c;u表示第几个儿子 0的根的节点编号为idx -------------------------------------------------------- 根是0的有个儿子c&#xff0c;编号为1的节点有个子节点为a&#xff0c;a的编号是2&#xf…

Mybatis 动态SQL – 使用choose标签动态生成条件语句

之前我们介绍了if,where标签的使用&#xff1b;本篇我们需要在if,where标签的基础上介绍如何使用Mybatis提供的choose标签动态生成条件语句。 如果您对if,where标签动态生成条件语句不太了解&#xff0c;建议您先进行了解后再阅读本篇&#xff0c;可以参考&#xff1a; Mybat…

解决C++ 遇笔试题输入[[1,2,3,...,],[5,6,...,],...,[3,1,2,...,]]问题

目录 0 引言1 思路2 测试结果3 完整代码4 总结 0 引言 现在面临找工作问题&#xff0c;做了几场笔试&#xff0c;遇到了一个比较棘手的题目就是题目输入形式如下&#xff1a; [ [3,1,1], [3,5,3], [3,2,1] ] 当时遇到这个问题还是比较慌的&#xff0c;主要是之前没有遇到这样的…

内网穿透实战应用-如何通过内网穿透实现远程发送个人本地搭建的hMailServer的邮件服务

文章目录 1. 安装hMailServer2. 设置hMailServer3. 客户端安装添加账号4. 测试发送邮件5. 安装cpolar6. 创建公网地址7. 测试远程发送邮件8. 固定连接公网地址9. 测试固定远程地址发送邮件 hMailServer 是一个邮件服务器,通过它我们可以搭建自己的邮件服务,通过cpolar内网映射工…

智慧园区能源管理系统可以本地私有化部署吗?

答案是肯定的&#xff0c;智慧园区能源管理系统可以本地私有化部署! 随着社会的发展和经济的增长&#xff0c;能源消耗逐渐成为影响社会发展的重要因素。为了更好地管理能源&#xff0c;提高能源利用效率&#xff0c;降低能源消耗成本&#xff0c;智慧园区能源管理系统应运而生…

Go语言在机器学习中有未来吗?

Go 是一种开源编程语言&#xff0c;最初由 Google 设计&#xff0c;用于优化系统级服务的构建和使用、在大型代码库上轻松工作&#xff0c;以及利用多核联网机器。 Go 于 2009 年推出&#xff0c;作为一种静态类型和编译型编程语言&#xff0c;深受 C 语言的影响&#xff0c;注…

idea 无法识别maven的解决

问题描述 从git拉取代码或者修改文件夹以后&#xff0c;整个项目所有依赖爆红无法通过修改或者重新加载maven解决版本为idea 2021 问题定位 maven的版本太高&#xff0c;而idea的般本太低&#xff0c;导致识别的时候稳定性差 解决 使用idea原生的maven版本 选择已捆绑的m…

win10 ping不通 Docker ip(解决截图)

背景&#xff1a; win10下载了docker desktop就是这个图&#xff0c;然后计划做一个springboot连接docker。 docker部署springboot :docker 部署springboot(成功、截图)_總鑽風的博客-CSDN博客 问题&#xff1a;spring boot部署docker后&#xff0c;docker接口通了&#xff0…

Tomcat 日志乱码问题解决

我就是三井&#xff0c;一个永不放弃希望的男人。——《灌篮高手》 Tomcat 日志乱码问题解决 乱码原因&#xff1a;字符编码不一致 如&#xff1a;国内电脑一般都是GBK编码&#xff0c;而Tomcat日志使用的是UTF-8编码 解决方法&#xff1a;将对应字符编码由 UTF-8 改为 GBK 即…