C++ 继承学习笔记

news2024/12/25 9:04:52

1.继承概念

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

基本格式:在新的类的后面,冒号+继承方式+基类

                     

父类必须是已经存在的类:

                                          

派生类也叫子类,基类也叫父类,子类从父类处“继承”父类的结构。

而继承的方式有public private protected三种方式(此处采用的public)

继承的逻辑:

继承就是将父类的全部拷贝一份,函数是同样的函数(就像类函数一样管理,这样能节省空间),但是变量是新的变量。

假设Person中有一个打印信息的Print函数,Print这种函数就不用自己在student重新写了,但是构造函数等还是得重新写(会在后文详细说明)。

继承的本质还是一种复用


2.继承方式

三种继承方式名称与访问限定符一致。

父类成员在派生类中到底是什么访问方式,取决于该变量在父类的访问限定符和子类的继承方式。

观察此表,可以得出规律: 

1. 基类的私有成员,无论派生类以什么方式继承都不可见

不可见的意思就是,派生类无论如何都不能使用这个类,不论你是访问还是写入

不可见的本质就是不想被子类继承。但是派生类中是有这个成员的,只是自己不能访问。

那么子类有没有方法能够访问父类中private下的成员呢?

解决方案:在父类中增加一个函数,而这个函数可以直接访问该被private修饰的成员 。

然后子类可以继承并使用这个函数即可。

           


关于私有继承(表格最右栏),本来的public成员也变private成员了。那么除了基类成员都是不可见之外,另外六种情况怎么处理呢?我们见规律如下:

2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问(需要能被可见化继承),就定义为protected。可以看出保护成员限定符是因继承才出现的
                                                  
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected
> private。

换句话说,根据  权限public大于protected大于private 就能判断此时到底属于什么。 

最后,还有两种经常使用的注意事项:

4. 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public 不过
最好显示的写出继承方式
5. 在实际运用中一般使用都是 public 继承, 几乎很少使用protetced/private继承 ,也不提倡
使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。

因此,实践中主要是这两种运用的比较多:用public继承Public 、用public继承protected

                                         


3. 基类和派生类对象赋值转换 (不会生成临时变量,直接切片)

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去
但是基类对象不能赋值给派生类对象。这也叫 赋值兼容转换

                              ​​​​​​​

这里不同于普通的隐式类型转换。指针和引用两种情况并不产生临时变量。因此Person&前面可以不加const(但是直接赋值p = s是有临时变量的

回顾:char c = 'x'; char& C = c;

这样使用是会报错的,因为引用赋值时会有临时变量,需要使用const修饰。

const char& C = c;

p=s: 将s中的相关的内容拷贝一份过来

Person* ptr = &s   ptr只指向基类的部分

                    

Person& ref = s      ref只作为派生类中的父类部分的引用。

                          


正是因为没有临时变量,所以改变变量p也会改变s中的内容。改变s中继承的内容也会改变p中的内容。

关于基类赋值给派生类(之后再学习):

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run
Time Type Information) dynamic_cast 来进行识别后进行安全转换。

4. 继承中的作用域 

在继承体系中 基类 派生类 都有 独立的作用域
通过一个例子来看继承中的查找规则:
例如,子类想调用一个Print函数,但是Print函数是实现在父类中的,编译器先直接在派生类类域中查找有无该函数,没有的话就直接去父类中查找,可以直接使用。
先找自己,再找父类,再找全局。

4.1 重定义&隐藏

同一个域是不能有同名变量的,但是基类和派生类作为两个域,是可以有重名变量的。
子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问 ,这种情况叫隐藏,
也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问

根据查找规则也可以理解,要先去找 自己的类域里面找,找不到再去父类的类域。

这种操作被叫做 重定义 或者 隐藏。

来看两个关于隐藏的题目:

第一题:

答案是B

两者构成隐藏。重载要求在同一个作用域。

如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
第二题:

          主函数是:                        

                                              

答案:构成隐藏,但是编译报错 。

编译器在编译时,先去子类中查找fun,找到了便直接使用,但是使用时发现参数名对不上,所以编译报错。

正确使用方法:

                                                 

在实际运用中,避免定义同名成员,纯纯给自己找坑。

但是同名成员是不可能完全避免的,因为运算符重载的名字不会改变。


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

取地址重载使用较少,我们对拷贝构造,赋值,默认构造和析构进行梳理:

父类的构造,子类可以用,但是这样足够吗?

C++规定,子类如果要进行构造,拷贝构造,析构,赋值等操作,父类的部分都必须调用父类相关的函数进行操作。

5.1 默认构造 

不写的话,编译器会自动生成一个默认构造。

此时子类中有三种变量:

                             

注意,要将父类成员看做整体。

先来观察下如果不在子类中写,编译器默认生成的是什么样子的:

调用了父类的构造和析构。 

因为父类会被当作一个整体,直接调用了父类的构造和析构。

在初始化时,也不能直接操作父类的对象,比如直接在初始化列表中走父类的_name: 

按照上图的报错,基类是可以整体初始化的

的确,可以让父类整体走初始化列表:

但是对于编译器自己生成的子类的构造函数,只能调用父类的默认构造。


5.2 拷贝构造

注意拷贝构造必须传引用:

class student : public person {
public:
	void func(int i) {
		cout << "func(int i)" << endl;
	}

	student(int grade = 100,int x =30) 
		://person(40,"qq")
		_grade(grade)
		,_x(x)
	{
		cout << "student()" << endl;
	}
	student(const student& s)
		:_grade(s._grade)
		,person(s)
	{
		cout << "student(const student& s)" << endl;
	}
private:
	size_t _grade;
	int _x;
};

再实现一个person的拷贝构造:

                           

难点就是:

                                          

s作为一个student类型的变量,是如何给person拷贝的?

根据刚才的对象赋值转换,当Person走拷贝构造时,直接使用s作为参数 ,s就会通过切片的方法传给p , 此时的p引用就不再是指向派生类全部,而是指向派生类中的父类部分(被切割了) 子类可以直接当父类用,会自动进行切片的操作。


5.3 赋值运算符重载:

先在父类中实现一个赋值运算符的现代写法:

class person {
public:
	person(int age = 20,string name = "peter",string _add = "武侯区")
		:_age(age)
		,_name(name)
		,_add(_add)
	{
		cout << "person()" << endl;
	}
	person(const person& p)
		:_age(p._age)
		,_name(p._name)
		,_add(p._add)
	{
		cout << "person(const person& p)" << endl;
	}
	person& operator=( person copy) {
		swap(copy._add, this->_add);
		swap(copy._name, this->_name);
		swap(copy._age, this->_age);
        return *this;
	}


	void func() {
		cout << "func()" << endl;
	}
protected:
	int _age;
	string _name;
	string _add;
};

同理,由于赋值转换,person对象也可以直接被student对象赋值

                                                    

      然后直接在基类部分复用父类的operator=即可

             ​​​​​​​


5.4 析构

       

先按照和上面同样的处理方法:

但是析构不同的是,~student和~Person都会被改名为destructor()  两者形成隐藏,需要指定~Person()的类域。

                       ​​​​​​​

这样写的程序虽然能跑,但是观察控制台可知,~student()打印的次数超过预计。 

这里的逻辑有不同,Person::~Person()是不需要我们显式写的 

对你没看错,又是cpp的特例,会在子类析构后自动调用父类的析构。

并且cpp也希望在析构时,先子后父。因为子类的析构中可能会用到父类的数据。

原因如下:

初始化时,先父后子

(因为子类构造初始化中可能会使用父类成员)

但是c++避开了这个问题,因为初始化列表的顺序不是真正的初始化顺序,而是根据声明的顺序来的。

析构则相反:

析构则遵守先子后父的规则。

又因为父类的显式调用不能保证先父后子,所以

观察派生类的构造函数是否满足上述称述:

是因为父类的初始化如果需要手动实现,只能在初始化列表里写,而初始化列表并不是根据显式写出的顺序来,而是能保证先父后子。

即使屏蔽掉person在初始化列表中的位置,也是先父后子 


 6. 友元

在类中被声明为友元的外部函数可以访问类里的私有成员

你爸的朋友不是你的朋友,友元函数不能够被继承。

比如:

Display实现如下: 

void Display(const Person& p, const Student& s)
{
 cout << p._name << endl;
 cout << s._stuNum << endl;
}

明显这个是不能跑的,Display是person的友元,能访问person的private 或者 protected

但是不能访问student中的protected或者private 

派生类的友元函数更不能访问基类!


7. 继承与静态成员

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

8.菱形继承及菱形虚拟继承(简易)

上文我们多介绍的是单继承,还有多继承:

                     ​​​​​​​ 


比如我们从一个植物的类继承出了水果和蔬菜,又想从水果和蔬菜里面去各继承一部分实现出西红柿。

              

写出来不会有问题,但是当你在西红柿里访问植物中的变量时就会报错。

因为植物的信息有两份继承到了西红柿里面(蔬菜一份,水果一份),会有歧义。

解决方案一:

                      

声明类域即可:

                                    

解决了二义性的歧义问题,但是没有解决数据冗余的问题。

并且此时的a还是有两个name,二义性也没有完全解决。


方案二:虚拟继承

也就是在选择继承方式的时候,在前面加上关键字virtual

虚拟继承只能在此处使用!

关于在哪写关键字vietual:

                                  

在哪继承会冗余,就在哪里写。所以当发生上图情况的时候,应当在B和C的位置用virtual

原理:虚基表

虚基表难度较大,博主争取在之后出一篇专门的文章梳理虚基表的内容。

不建议菱形继承,势必涉及到虚拟继承,使用多继承即可。

但是实践中也会有菱形继承:

                             

ios继承给istream和ostream  istrem和ostream又继承给iostream

当然,库里面肯定是采用虚拟继承的。

9. 继承与组合

继承的总结和反思
1. 多继承就是cpp难度大的一个体现。有了多继承 ,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是 C++ 的缺陷之一,很多后来的 OO 语言都没有多继承,如 Java
3. 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
   
下面是一段网上的总结:
优先使用对象组合,而不是类继承
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用 (white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。 
白盒复用和黑盒复用是软件工程的概念,白盒即内部逻辑不可见,黑盒即内部逻辑可见
工作中,白盒测试难度远大于黑盒测试,白盒测试一般由开发人员自己完成,黑盒由测试人员完成。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
(black-box reuse) ,因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。

博主在阅读完之后,有以下感受:

实践工作中,非常看重:高内聚,低耦合

便于团队协作,不要让新的板块影响已经写好的板块。

更符合is_a就用继承  比如:植物 水果

更符合has_a就用轮胎。 比如 汽车 轮胎

用面向对象的逻辑来决定继承还是组合。

再比如说,用list实现queue,可以说queue包含了一个链表,也可以说queue是一个链表

                                    

两种情况都能用的情况下,优先使用组合:has_a

比如list实现queue的时候我们就是使用的典型的has_a

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

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

相关文章

奥威让您更懂现金流情况

企业现金流一旦出了问题都是大问题&#xff0c;会直接影响到企业的日常运作&#xff0c;甚至直接关系到企业能不能继续存活&#xff0c;因此现金流量表是企业财务分析中重要报表之一&#xff0c;也是企业监控财务监控情况的重要手段之一。那么这么重要的一份现金流量表该怎么做…

科研绘图系列:R语言折线图(linechart plots)

文章目录 介绍加载R包导入数据数据预处理画图组合图形介绍 在R语言中,折线图(Line Plot)是一种常用的数据可视化类型,用于展示数据随时间或有序类别变化的趋势。折线图通过连接数据点来形成一条或多条线,这些线条可以清晰地表示数据的变化方向、速度和模式。 加载R包 k…

基于Spring Boot的宠物领养系统的设计与实现

基于Spring Boot的宠物领养系统的设计与实现 springboot138宠物领养系统的设计与实现 摘 要 如今社会上各行各业&#xff0c;都在用属于自己专用的软件来进行工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。互联网的发展&#xff0c;离不开一…

第 1 章:原生 AJAX

原生AJAX 1. AJAX 简介 AJAX 全称为 Asynchronous JavaScript And XML&#xff0c;就是异步的 JS 和 XML。通过 AJAX 可以在浏览器中向服务器发送异步请求&#xff0c;最大的优势&#xff1a;无刷新获取数据。AJAX 不是新的编程语言&#xff0c;而是一种将现有的标准组合在一…

JavaWeb案例

环境搭建 先创建好数据库&#xff0c;建表并插入数据 create database talis; use talis;-- 部门管理 create table dept(id int unsigned primary key auto_increment comment 主键ID,name varchar(10) not null unique comment 部门名称,create_time datetime not null com…

Springboot整合【Kafka】

1.添加依赖 在pom.xml文件中添加以下依赖&#xff1a; <!-- 进行统一的版本管理--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.3</version>&l…

【全网最全】2024年数学建模国赛A题30页完整建模文档+成品论文+代码+可视化图表等(后续会更新)

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片&#xff0c;那是获取资料的入口&#xff01; 2024年高教社杯数学建模国赛A题“板凳龙”闹元宵&#xff1a;建立舞龙队的运动轨迹和速度的空间几何、运动学和优化模型 本文文章较长&#xff0c;建议先看…

ardupilot开发 --- MQTT 篇

原图&#xff1a;ardupilot-onboardComputer-4Glink-console.drawio 白嫖党请点赞、收藏、关注 你说在一起要算命 前言参考文献 前言 为什么在ardupilot开发过程中要用到MQTT &#xff1f; 客户要求向他们的指挥中心平台推送视频流和飞控数据&#xff0c;即要将图数传数据推送给…

代码随想录:96. 不同的二叉搜索树

96. 不同的二叉搜索树 class Solution { public:int numTrees(int n) {int dp[30]{0};//由i个结点组成的二叉搜索树有多少种dp[0]1; for(int i1;i<n;i)for(int j0;j<i;j)//j表示根节点左子树有j个结点dp[i]dp[j]*dp[i-j-1];//对根节点左右子树结点数量遍历//数量有左子树…

【计算机网络】TCP连接如何确保传输的可靠性

一、确保可靠传输的机制 TCP&#xff08;传输控制协议&#xff09;是一种面向连接的、提供可靠交付的、面向字节流的、支持全双工的传输层通信协议 1、序列号 seq TCP头部中的序号&#xff0c;占32位&#xff08;4字节&#xff09;&#xff1b; 发送方给报文段分配一个序列号&a…

CSS中 特殊类型的选择器 伪元素如何使用

一、什么是伪元素 在 CSS 中&#xff0c;伪元素是一种特殊类型的选择器&#xff0c;它允许你为元素的特定部分添加样式&#xff0c;而这些部分在 HTML 文档中并不实际存在。伪元素通常用于创建装饰性效果&#xff0c;如添加边框、背景、阴影等&#xff0c;而不需要额外的 HTML…

PHPJWT的使用

今天得空整理整理JWT的代码 首先&#xff0c;我们得知道什么是JWT&#xff1f; JWT&#xff08;JSON Web Token&#xff09;是一种开放标准&#xff08;RFC7519&#xff09;&#xff0c;用于在网络应用环境中安全地传输声明信息。它是一种紧凑的、URL安全的令牌格式&#xff0…

(一)使用Visual Studio创建ASP.NET Core WebAPI项目

1.创建webAPI项目 选择ASP.NET Core Web API项目模版&#xff08;基于.Core框架可以支持多种系统环境&#xff0c;所以我们选择.Core框架&#xff09;&#xff0c;点下一步。 2.项目名称 项目名称设置为&#xff1a;CoreWebAPI&#xff0c;点下一步 3.选择框架 选择.NET6.0框…

分类预测|基于黑翅鸢优化轻量级梯度提升机算法数据预测Matlab程序BKA-LightGBM多特征输入多类别输出 含对比

分类预测|基于黑翅鸢优化轻量级梯度提升机算法数据预测Matlab程序BKA-LightGBM多特征输入多类别输出 含对比 文章目录 一、基本原理BKA&#xff08;Black Kite Algorithm&#xff09;的原理LightGBM分类预测模型的原理BKA与LightGBM的模型流程总结 二、实验结果三、核心代码四、…

IP学习——twoday

双层Vlan标签 路由器常用命令&#xff1a; 查看当前端口&#xff0c;路由等的信息和配置&#xff1a;display this 查看当前路由器的所有信息&#xff1a; display current-configuration 查看当前路由器的指定信息&#xff1a; display current-configuration | include ip a…

HTML第一课 语法规范与常用标签

目录 ◆ HTML 语法规范 ◆ HTML 常用标签 4.2 标题标签 4.3 段落和换行标签 4.4文本格式化标签 4.5<div>和<span>标签 4.6图像标签和路径 4.7超链接标签 1.外部链接 2.内部链接 3.空链接 4.下载链接 5.锚点链接 ◆ HTML 中的注释和特殊字符​编辑 ◆ HTML 语…

Redis中String类型的基本命令

文章目录 一、String字符串简介二、常见命令setgetmgetmsetsetnxincrincrbydecrdecrbyincrbyfloatappendgetrangesetrangestrlen 三、命令小结四、字符串内部编码五、String典型使用场景1. 缓存(Cache)功能2. 计数功能3. 共享会话&#xff08;Session&#xff09;4. 手机验证码…

软件测试学习笔记丨Pytest+Allure测试计算器

本文转自测试人社区&#xff0c;原文链接&#xff1a;https://ceshiren.com/t/topic/31954 项目要求 3.1 项目简介 计算器是近代人发明的可以进行数字运算的机器。 计算器通过对加法、减法、乘法、除法等功能的运算&#xff0c;将正确的结果展示在屏幕上。 可帮助人们更方便的…

FLTRNN:基于大型语言模型的机器人复杂长时任务规划

目录 一、引言二、FLTRNN框架2.1 任务分解2.2 基于语言的递归神经网络&#xff08;Language-Based RNNs&#xff09;长期记忆&#xff08;Long-Term Memory, Ct&#xff09;&#xff1a;短期记忆&#xff08;Short-Term Memory, Ht&#xff09;&#xff1a; 2.3 增强推理能力的…

GAMES104:12 游戏引擎中的粒子和声效系统-学习笔记

文章目录 一&#xff0c;粒子基础Particle System二&#xff0c;粒子渲染三&#xff0c;GPU粒子及生命周期控制四&#xff0c;粒子应用五&#xff0c;声音基础5.1 Sound System5.2 Digital Sound5.3 Audio Rendering QA 一&#xff0c;粒子基础Particle System 网游里你的付费…