《类和对象--继承》

article/2025/7/2 1:23:51

引言:

在刚接触C++的时候,我们首先学习了有关类和对象的一些基础知识,今天我们就要接着学习类和对象的另一板块–继承。

一:继承的概念和定义

1. 继承的概念

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

下面看一个例子来感受继承的作用:

下面我们看到没有继承之前我们设计了两个类StudentTeacherStudentTeacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
在这里插入图片描述
在这里插入图片描述

下面我们公共的成员都放到Person类中,Studentteacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 继承的定义

(1)定义格式:

下面我们看到Person是基类,也称作父类。Student是派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类。
在这里插入图片描述

(2)继承机制与访问限定符:

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

(3) 继承基类成员访问方式的变化:

在这里插入图片描述

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见的,基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式)public>protected>private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式为public,不过最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
三种继承方式对比:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到这里只有公有继承在类外才能直接访问基类中的公有成员。

二:基类和派生类间的转换

  1. public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针引用指向的是派生类中切出来的基类那部分。
  2. 基类对象不能赋值给派生类对象
  3. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time?Type?Information)的dynamic_cast来进行识别后进行安全转换。(ps:这个我们后面类型转换章节才会学习,这里先提一下)

在这里插入图片描述

代码演示:

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

三:继承中的作用域

1. 隐藏规则:

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

在这里插入图片描述

2. 考察继承作用域相关选择题:

  1. A和B类中的两个func构成什么关系()
    A. 重载 B. 隐藏C. 没关系
  2. 下面程序的编译运行结果是什么()
    A. 编译报错B. 运行报错 C. 正常运行

class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
};
解析:
首先根据继承中的规则,只要成员函数名相同就构成隐藏,因此第一题是隐藏关系
第二题中会出现编译报错,因为编译的时候,编译器先会到B中查找fun(int)这个函数,第一个语句传参调用没问题,第二个语句没有传参就会报错,因为这里编译器找到的还是B中的fun(int),那么这是为什么呢?因为两者函数名字相同,构成了隐藏,编译器在B中只会找到找到fun(int),而不会找到fun(),如果想要找到的话就需要指明类域。
在这里插入图片描述

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

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

1. 派生类的默认构造函数:

在初始化时,当基类存在默认构造函数时就不需要在派生类中显示调用了。
编译器会自动调用其默认构造函数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当基类中没有默认构造函数时:
在这里插入图片描述

可以看到编译器在这里报错了,因为基类中没有合适的默认构造函数
这个时候我们就需要显示调用构造函数:
在这里插入图片描述

(1) 派生类中默认构造函数的细节

这里还要注意一点:当我们不写默认构造函数,用编译器默认生成的默认构造函数时,如果在派生类中不显示调用的话,编译器的初始化规则遵循之前的规则:
内置类型我们理解为不做处理,自定义类型会调用其默认构造函数。

先来看不显示调用:
在这里插入图片描述
在这里插入图片描述

再来看显示调用:
在这里插入图片描述
注意这里显示调用的写法。

(2) 小结:

对于基类的那部分成员,就可以当做一个自定义类型来理解,当其存在默认构造函数时,编译器就会自动调用其默认构造,当没有默认构造时就需要我们显示调用。

2. 派生类的拷贝构造函数:

用法如下:
在这里插入图片描述
在这里插入图片描述
这里有同学可能会有困惑,这里拷贝构造基类成员_name的时候为什么这样写呢?
注意要把继承的基类成员看成一个整体,那么这里在调用拷贝构造的时候要对一个整体进行拷贝构造,但是那么多成员要怎么搞呢?我们同学可能会想:那我传入一个代表成员吧,但是我们的祖师爷在设计的时候没有这么想,而是直接将派生类对象传入,但是派生类对象里面除了基类成员还有自己的成员啊,这怎么行呢?其实这个过程中会进行切片操作,这个就是编译器的工作了。
下面来感受一下这个过程:
在这里插入图片描述

3. 派生类的operator=

如果让我们来写派生类的operator= 的话,一开始我们可能会这样写:
在这里插入图片描述

但是这里可以看到出错了。

之后通过调试我们发现在进行赋值操作的时候这里陷入了死循环。
在这里插入图片描述

这是为什么呢?其实不难看出因为这里的operator= 和基类中的operator= 名字相同,
构成了隐藏,因此这里我们在调用operator= 时调用的不是基类中的operator= ,这里调用的还是派生类中的自己,因此陷入了死循环,那么如何解决这个问题呢?只需要指明类域即可。
在这里插入图片描述

4. 派生类的析构函数:

根据之前的经验我在写派生类的析构函数的时候应该会这样写:
在这里插入图片描述

但运行之后会发现基类的析构函数调用了两次,这肯定是不合理的,如果有资源存在的话释放两次是一定会出错,那么这是为什么呢?

这里其实就是因为由于要做到析构顺序先子后父,但是显示调用基类的析构函数的话不能完全保证先子后父,因此这里就不用自己显示调用基类的析构函数了,编译器会自动调用。
在这里插入图片描述

5. 实现一个不能被继承的类

方法1:将基类的构造函数私有。
原因:派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。

方法一:

在这里插入图片描述

方法二:

在这里插入图片描述

五:继承与友元

友元关系不能继承,也就是说基类中的友元不一定能够访问派生类中的成员和变量
可以理解为:你爸的朋友不一定是你的朋友。

在这里插入图片描述
如果想访问的话还需要在派生类中写成友元:
在这里插入图片描述

六:继承与静态成员

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

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

七:多继承及菱形继承问题

1. 继承模型:

  1. 单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
  2. 多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
  3. 菱形继承菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

这是单继承的一个示意图:
在这里插入图片描述
这个是一个多继承的示意图:
在这里插入图片描述
在这里插入图片描述

2. 二义性问题:

在这里插入图片描述
在这里插入图片描述
可以看到这里出现了数据的二义性问题,但是这个问题是可以解决的:

在这里插入图片描述
这里指明类域就可以解决数据的二义性问题,但是上面的数据冗余问题就无法解决。

那么有什么方法可以同时解决这两个问题呢?那么就看下面的虚继承:

3. 虚继承:

很多人说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java
在这里插入图片描述
在这里插入图片描述
这里需要注意的就是要在发生菱形继承的源头进行虚继承。

4. 多继承中的指针偏移问题:

下面说法正确的是()
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D: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;
}
解析:
根据其继承规则,大概的示意图如下:
在这里插入图片描述

5. IO库里的菱形继承:

在这里插入图片描述

6.小结:

尽量不要去玩菱形继承。

八:继承和组合

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

完结!!!


http://www.hkcw.cn/article/uHgmIEfZUm.shtml

相关文章

利用R语言生成区试中随机区组试验设计——多点

目前&#xff0c;区试要求对照不得位于区组的首尾小区&#xff0c;且不同区组的相邻小区位置不得出现同一品种。基于这一要求&#xff0c;编写了R语言的随机区组试验设计。此函数可用于多个试验点试验设计生成情况。 rcbd函数有6个参数&#xff1a; local_name为试验点名称的向…

pytorch基本运算-范数

引言 前序学习进程中&#xff0c;已经对pytorch基本运算有了详细探索&#xff0c;文章链接有&#xff1a; 基本运算 广播失效 乘除法和幂运算 hadamard积、点积和矩阵乘法 上述计算都是以pytorch张量为运算元素&#xff0c;这些张量基本上也集中在一维向量和二维矩阵&#x…

STM32G4 电机外设篇(四)DAC输出电流波形 + CAN通讯

目录 一、STM32G4 电机外设篇&#xff08;四&#xff09;DAC输出电流波形 CAN通讯1 DAC输出电流波形1.1 STM32CubeMX配置和Keil代码1.2 实验现象 2 CAN/CANFD通讯2.1 STM32CubeMX配置和Keil代码2.2 实验现象 附学习参考网址欢迎大家有问题评论交流 (* ^ ω ^) 一、STM32G4 电机…

电子电气架构 --- 后轮转向的一点事情

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 做到欲望极简&#xff0c;了解自己的真实欲望&#xff0c;不受外在潮流的影响&#xff0c;不盲从&#x…

web复习(四)

盒子模型的例题 例一&#xff1a; <!doctype html> <html> <head> <meta charset"utf-8"> <title>咖啡店banner</title> <style type"text/css"> /*将页面中所有元素的内外边距设置为0*/ *{ padding:0; margin…

Cesium添加点线面(贴地)

// 创建一个图元集合const primitives viewer.scene.primitives.add(new Cesium.PrimitiveCollection());1、点上图 // 定义点的位置&#xff08;中国不同城市的经纬度&#xff09;const points [{ lon: 116.4074, lat: 39.9042, name: "北京" },{ lon: 121.4737, …

技术文档:MD520系列变频器配套杭州干扰净GRJ9000S系列EMC电源滤波器安装指南

1. 引言 MD520系列通用变频器是汇川技术有限公司&#xff08;Inovance&#xff09;设计的高性能电流矢量控制交流驱动器&#xff0c;广泛应用于纺织、造纸、机床、包装、食品、风机和水泵等行业。为确保其在复杂电磁环境中稳定运行并不对其他设备造成干扰&#xff0c;手册推荐…

【基于阿里云搭建数据仓库(离线)】DataWorks中删除节点

1.右击想要删除的节点&#xff0c;点击删除 2. 显示如下界面&#xff0c;点击“去下线” 3.进入到如下界面&#xff0c;点击红色框出来的 4.重新右击想要删除的目标节点&#xff0c;点击删除 5. 点击去下线 6.点击开始下线 7.点击“确认发布” 8.点击“确认” 9.点击“重新删除…

【GESP真题解析】第 6 集 GESP 三级 2023 年 9 月编程题 1:小杨的储蓄

大家好,我是莫小特。 这篇文章给大家分享 GESP 三级 2023 年 9 月编程题第 1 题:小杨的储蓄。 题目链接 洛谷链接:B3867 小杨的储蓄 一、完成输入 根据输入格式的描述,输入有两行,第一行为两个整数 N 和 D,数据范围: 1 ≤ N ≤ 1000 1\le N \le 1000 1≤N≤1000, 1 …

MySQL-多表关系、多表查询

一. 一对多(多对一) 1. 例如&#xff1b;一个部门下有多个员工 在数据库表中多的一方(员工表)、添加字段&#xff0c;来关联一的一方(部门表)的主键 二. 外键约束 1.如将部门表的部门直接删除&#xff0c;然而员工表还存在其部门下的员工&#xff0c;出现了数据的不一致问题&am…

Arbitrum Stylus 合约实战 :Rust 实现 ERC721

在上一篇中&#xff0c;我们学习了如何在 stylus 使用 rust 编写 ERC20合约&#xff0c;并且部署到了Arbitrum Sepolia &#xff0c;今天我们继续学习&#xff0c;如何在 stylus 中使用 rust 实现 ERC721 合约&#xff0c;OK, 直接开干&#xff01; 关于环境准备&#xff0c;请…

超声波测距三大算法实测对比

前言 声波测距的数据包含很大噪声&#xff0c;即使障碍物&#xff08;以纸板为例&#xff09;静止&#xff0c;测量距离数据也上下跳变&#xff0c;需要通过数据滤波算法降低测量误差&#xff0c;主要滤波算法有平均值滤波和卡尔曼滤波。 在超声波测距中&#xff0c;无滤波、…

【2025年5月】AI生产力再探再报:各家智能体持续内卷,前沿应用不断细分

前言 2025年5月的个人学习笔记。 一、工具尝鲜快报&#xff1a;初探感觉好玩&#xff0c;但还未深入的工具。 二、生产力军火库&#xff1a;开箱即用的神器&#xff0c;以及一些好用的技巧。 三、前沿动态速递&#xff1a;一些可反复品读的优质资料和个人感兴趣的新工具。 文章…

ubuntu22.04安装megaton

前置 sudo apt-get install git cmake ninja-build generate-ninja安装devkitPro https://blog.csdn.net/qq_39942341/article/details/148388639?spm1001.2014.3001.5502 安装cargo https://blog.csdn.net/qq_39942341/article/details/148387783?spm1001.2014.3001.5501 …

shell脚本的条件测试

命令结果判定 && &#xff1a;在命令执行后如果没有任何报错时会执行符号后面的动作 || &#xff1a;在命令执行后如果命令有报错会执行符号后的动作 条件判断 # test 语句 # []&#xff0c;[[]]&#xff0c;(()) 语句 # [[]] 可以支持的表达式更多&#xff0c;是最常…

已有的前端项目打包到tauri运行(windows)

1.打包前端项目产生静态html、css、js 我们接下来用vue3 vite编写一个番茄钟案例来演示。 我们执行npm run build 命令产生的dist目录下的静态文件。 2.创建tarui项目 npm create tauri-applatest一路回车&#xff0c;直到出现。 3.启动运行 我们将打包产生的dist目录下的…

随记 nacos + openfegin 的远程调用找不到服务

这里的配置问题就不说了&#xff0c;基本的都没有问题&#xff0c;然后现在的是怎么样的场景呢&#xff0c;就是有两台服务器&#xff0c;两台服务器分别部署了两个模块&#xff0c;B要调用A服务&#xff0c;然后通过nacos找到了这个服务的名称&#xff0c;但是呢发现连不上&am…

【Python 算法零基础 4.排序 ⑦ 桶排序】

草木不争高&#xff0c;争的是生生不息 —— 25.5.26 选择排序回顾 ① 遍历数组&#xff1a;从索引 0 到 n-1&#xff08;n 为数组长度&#xff09;。 ② 每轮确定最小值&#xff1a;假设当前索引 i 为最小值索引 min_index。从 i1 到 n-1 遍历&#xff0c;若找到更小元素&am…

天机学堂-分页查询

需求 分页查询我的课表 返回&#xff1a; 总条数、总页数、当前页的课表信息的集合 返回的VO&#xff08;已经封装成统一的LearningLessonsVO&#xff09; 定义Controller RestController RequestMapping("/lessons") RequiredArgsConstructor public class Lear…

Transformer 是未来的技术吗?

之前的文章中&#xff0c;聊了不少关于 Transformer 方面的内容&#xff1a; Transformer 中的注意力机制很优秀吗&#xff1f;-CSDN博客初探 Transformer-CSDN博客来聊聊Q、K、V的计算-CSDN博客 现在的大模型基本都是基于 Transformer 或者它的演进技术&#xff0c;那么&…