【继承】—— 我与C++的不解之缘(十九)

news2025/2/12 4:03:45

前言:

面向对象编程语言的三大特性:封装继承多态

本篇博客来学习C++中的继承,加油!

一、什么是继承?

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

简单来说,对于两个类(甚至多个),它们直接有一些相同的成员;这样设计是有些冗余的。

比如下面这连个类,学生和老师,它们之间有很多相同的成员变量(函数):

class Student
{
public:
	void studying() {}  //学习
	void identity() {}  //进校园
private:
	string _name; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话

	int _id; //学号
};
class Teacher
{
public:
	void teaching() {}  //教学
	void identity() {}  //进校园
private:
	string _name; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话

	string _title; // 职称
};

​ 对于这种情况,我们就可以将这些公共成员放到一个类Person 中,让StudentTeacher 去继承Person 类的这些公共成员。

class Person
{
	void identity() {} //进学校

protected:
	string _name; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
};

class Student :public Person
{
public:
	void study() {};
private:
	string _id; //学号
};

class Teacher : public Person
{
	void teaching() {} //教学
private:
	string _title; //职称
};

这样就形成了下图这样的继承关系

在这里插入图片描述

二、继承的定义

​ 我们看到Person是基类,也称作父类。Student是派⽣类,也称作子类。(既叫基类/派生类,也叫父类/子类)

1、定义格式

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

2、继承基类成员访问方式

​ 在看基类成员访问方式之前,先来看一个在【C++类和对象(上)】—— 我与C++的不解之缘(三)-CSDN博客 提到的访问限定符,当初没有去了解它是啥,现在来看一下

在这里插入图片描述

//基类
class A
{
public:
	int _i1 = 1;
protected:
	int _i2 = 2;
private:
	int _i3 = 3;
};
// public 继承
//派生类
class B : public A
{
public:
	void func1()
	{
		//基类中的public成员,派生类中可以直接访问,在类外也可以直接访问
		cout << _i1 << endl;
	}
	void func2()
	{
		//基类中的protected成员,派生类中可以直接访问,在类外不能直接访问
		cout << _i2 << endl;
	}
	//void func3()
	//{
	//	//基类中的private成员,派生类中不可见, 派生类中和类外都不能访问
	//	cout << _i3 << endl;
	//}
};
void test1()
{
	B bb;
    //基类的public成员, 在类外可以直接访问
	cout << bb._i1 << endl;
	bb.func2();
}

在这里插入图片描述

基类成员 / 继承方式public 继承protected 继承private 继承
public 成员派生类中的public成员派生类中的protected成员派生类中的private成员
protected 成员派生类中的protected成员派生类中的protected成员派生类中的private成员
private 成员在派生类中不可见在派生类在不可见在派生类在不可见
  1. 基类private 成员在派生类中无论以什么方式继承都是不可见的;(这里不可见指的就是私有成员还是被继承到派生类当中去,但是语法上,无论是在派生类里面还是类外面都不能直接去访问它。
  2. ​ 基类private成员在派生类中不能被访问;如果基类成员只是不想在类外直接被访问,而在派生类中能被访问,就定义为protected。(可以看出保护成员限定符protected就是因继承而出现的)。
  3. ​ 根据上面表格,可以发现: 基类的private 成员在派生类当中都是不可见的。基类的其他成员在派生类发访问方式 就等与 成员在基类的访问限定符与继承方式之中最小的 public > protected > private
  4. ​ 在实际应用中,一般都是使用public 继承,很少使用protectedprivate 继承(不推荐,因为protectedprivate 继承下来的成员只能在派生类中使用,实际中扩展维护性不是很强)。

3、继承类模板

​继承类模板:
如果继承的基类是类模板,则需要指定类域;否则就会报错。

这里使用继承来实现一下stack

template<class T>
class stack : public vector<T>
{
	//这里编译器是按需进行实例化,这里只是实例化出vector<T>
	//里面对应的成员函数等,都是按需实例化,在需要时才进行实例化
	//所以需要指定类域来访问vector<T> 的成员函数
	void push(const T& x)
	{
		// 模版是按需实例化,push_back等成员函数未实例化,所以找不到
		vector<T>::push_back(x);
	}
	void pop()
	{
		vector<T>::pop_back();
	}
	const T& top()
	{
		return vector<T>::back();
	}
	bool empty()
	{
		return vector<T>::empty();
	}
};

三、基类和派生类之间的转换

  • public 继承的派生类对象,可以赋值给基类的 指针/引用 ;这种我们可以形象的称为**切片(或者切割) **,简单来说就是将派生类中基类的那一部分切出来,基类的指针/引用 指向派生类中切出来的那一部分。
  • 基类对象不能赋值给派生类对象
  • 基类的指针/引用 可以通过强制转换赋值给派生类的指针和引用 ,但必须是基类的指针指向派生类对象时才是安全的。(这里如果基类是多态类型,可以使用RTTIdynamic_cast 进行识别后进行安全转换。以后再了解这部分内容)

在这里插入图片描述

void test2()
{
	Student sobj;
	// 1.派⽣类对象可以赋值给基类的指针/引⽤
	Person* pp = &sobj;
	Person& rp = sobj;
	// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
	Person pobj = sobj;
	//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
	//sobj = pobj;
}

四、继承中的作用域

1、隐藏规则

  • 在继承中,基类和派生类都有独立的作用域。
  • 基类和派生类中有同名函数时,派生类将屏蔽基类的同名函数的直接访问(这种情况叫做隐藏)(可以和后面多态中的覆盖/重写对比记忆,在多态章节的博客中详细讲解)。
  • 注意: 对于成员函数,只要函数名相同就构成隐藏。
  • 实际中最好不定义同名函数。
class person
{
protected:
	string _name = "小晓";
	int _num = 111;
};
class student :public person
{
public:
	void test()
	{
		cout << "_name: " << _name << endl;
		cout << "_num: " << _num << endl;
		cout << "person::_num: " << person::_num << endl;
	}
protected:
	int _num = 999;
};
void test3()
{
	student s1;
	s1.test();
}
int main()
{
	test3();
	return 0;
}

2、相关选择题解析

在这里插入图片描述


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

​ 首先,对于成员函数,只要函数名相同就构成隐藏,使用A类与B类中fun 函数构成隐藏。

对于第二道题,我第一时间想法:正常运行,(个人理解:B类中继承了A类的fun() 成员函数,又实现了自己的成员函数fun(int) 形成了函数重载);很显然这种理解是错误的,(在继承中,派生类中会屏蔽基类中同名函数的直接访问);我们无法通过派生类对象直接访问基类的成员函数。

在这里插入图片描述

五、派生类的默认成员函数

1、默认成员函数

现在再来看一下默认成员函数:
在这里插入图片描述

所谓默认成员函数(6个),就是我们不写编译器会自动生成;那在派生类当中,这写默认成员函数是如何生成的呢。

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

2、番外篇:实现一个不能被继承的类

方法一: 基类的构造函数私有,派生类的构造必须调用基类的构造函数,但是基类的构造函数私有化,派生类就不能调用,就无法实例化出对象。

方法二 C++11 新增关键字final, final修饰基类,派生类就不能继承的。

方法一:

//基类构造函数私有化
class Test
{
public:
	void func()
	{
		cout << "构造函数私有化,派生类无法实例化出对象" << endl;
	}
protected:
	int _i = 1;
private:
	Test()
	{}
};

class Fun : public Test
{
public:
	Fun()
	{}
private:
	char _ch;
};

在这里插入图片描述

方法二:

//final修饰
class Base final
{
public:
	void fun()
	{
		cout << "final修饰,无法被继承" << endl;
	}
protected:
	int _i;
	char _ch;
};

class Div : public Base
{

};

在这里插入图片描述

六、继承中的友元

首先,友元关系不能被继承;也就基类的友元函数无法访问派生类的私有和保护成员。

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Func(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}

在这里插入图片描述

七、继承中的静态成员

​ 基类定义了static 静态成员,在整个继承体系中有且只有一个这样的成员;无论有多少派生类都只有一个static 静态成员。

其实这个也很好理解,静态成员存储在常量区,只存在一个。

八、多继承与菱形继承

1、继承模型

单继承: 一个派生类只有一个基类

多继承: 应该派生类有两个或以上直接基类;(多继承对象在内存中的模型:先继承的在基类在前面,后继承的基类在后面,派生类成员放到最后。

**菱形继承: ** 菱形继承属于多继承的一种特殊情况,菱形继承存在数据冗余和二义性的问题;在Assistant 的对象中,Person 类的成员会存在两份。(多继承,一定会存在菱形继承;java 中不支持多继承就规避了这个问题;所以不推荐设计出这种菱形继承的模型)。

在这里插入图片描述

在这里插入图片描述

2、虚继承

虚继承可以说是解决菱形继承这个问题的;

​ 很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有

菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可

以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。

class Person
{
public:
	string _name;
	int _age;
	string _address; 
};
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:
	int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
	int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 使⽤虚继承,可以解决数据冗余和⼆义性
	Assistant a;
	a._name = "peter";
	return 0;
}

简单来说,不推荐设计出菱形继承。(设计出菱形继承就会复杂很多,所以还是尽量避免)。

3、IO库中的虚拟菱形继承

在这里插入图片描述

template < class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios < CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};

九、继承与组合

  • public继承是一种is-a;就是每一个派生类对象都是一个基类对象
  • 组合呢,是has-a的关系;这里假设B组合了A,每一个B对象中都有一个A对象。
  • 继承中允许根据基类的实现来定义派生类的实现;这种通过生成派生类的复用通常被称为白箱复用;(**白箱复用相对于可视性而言:**在继承方式中,基类的内部细节对派生类可见;继承一定程度上破坏了基类的封装,基类的改变对派生类有很大影响;基类和派生类之间耦合性高,依赖性很强)。
  • 组合,是类继承之外的另一种复用选择;新的更复杂的功能可以通过组合对象来实现,对象组合要求被组合的对象具有良好定义的接口;这种复用风格被称为黑箱复用;(对象的内部细节不可见,组合类之间没有很强的依赖关系,耦合度低,使用对象组合,有助于保持每个类的封装。
  • 优先去使用组合,而不是继承,组合耦合度低,代码维护性好;不过类之间如果关系适合继承(is-a)那就使用继承,多态也要实现继承;

继承部分到这里就结束了,感谢各位的支持,继续加油!!!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

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

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

相关文章

【目标跟踪】Anti-UAV数据集详细介绍

Anti-UAV数据集是在2021年公开的专用于无人机跟踪的数据集&#xff0c;该数据集采用RGB-T图像对的形式来克服单个类型视频的缺点&#xff0c;包含了318个视频对&#xff0c;并提出了相应的评估标准&#xff08;the state accurancy, SA)。 文章链接&#xff1a;https://arxiv.…

偏差-方差权衡(Bias–Variance Tradeoff):理解监督学习中的核心问题

偏差-方差权衡&#xff08;Bias–Variance Tradeoff&#xff09;&#xff1a;理解监督学习中的核心问题 在机器学习中&#xff0c;我们希望构建一个能够在训练数据上表现良好&#xff0c;同时对未见数据也具有强大泛化能力的模型。然而&#xff0c;模型的误差&#xff08;尤其…

Figma入门-原型交互

Figma入门-原型交互 前言 在之前的工作中&#xff0c;大家的原型图都是使用 Axure 制作的&#xff0c;印象中 Figma 一直是个专业设计软件。 最近&#xff0c;很多产品朋友告诉我&#xff0c;很多原型图都开始用Figma制作了&#xff0c;并且很多组件都是内置的&#xff0c;对…

Windows系统怎么把日历添加在桌面上用来记事?

在众多电脑操作系统中&#xff0c;Windows系统以其广泛的用户基础和强大的功能&#xff0c;成为许多人的首选。对于习惯于在电脑前工作和学习的用户来说&#xff0c;能够直接在桌面上查看和记录日历事项&#xff0c;无疑会大大提高工作效率和生活便利性。今天&#xff0c;就为大…

蓝桥杯备赛笔记(一)

这里的笔记是关于蓝桥杯关键知识点的记录&#xff0c;有别于基础语法&#xff0c;很多内容只要求会用就行&#xff0c;无需深入掌握。 文章目录 前言一、编程基础1.1 C基础格式和版本选择1.2 输入输出cin和cout&#xff1a; 1.3 string以下是字符串的一些简介&#xff1a;字符串…

大数据新视界 -- 大数据大厂之 Hive 数据压缩:优化存储与传输的关键(上)(19/ 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

RNN And CNN通识

CNN And RNN RNN And CNN通识一、卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;CNN&#xff09;1. 诞生背景2. 核心思想和原理&#xff08;1&#xff09;基本结构&#xff1a;&#xff08;2&#xff09;核心公式&#xff1a;&#xff08;3&#xff09;关…

求整数的和与均值

求整数的和与均值 C语言代码C 代码Java代码Python代码 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 读入n&#xff08;1 < n < 10000&#xff09;个整数&#xff0c;求它们的和与均值。 输入 输入第一行是一个整数n&#xff0c;…

配置idea环境进行scala编程

这里用的jdk是jdk-8u161,scala版本是2.12.0 在d盘新建一个本地仓库用来存放下载的maven包&#xff0c;在里面创建如下两个文件 更改settings文件为下面的样子 点击左下角的设置&#xff0c;更改maven本地仓库的位置&#xff08;默认在c盘用户目录下的.m2文件中&#xff0c;更改…

WSL简介与安装流程(Windows 下的 Linux 子系统)

目录 1.wsl安装 1.1 WSL简介 1.1.1 WSL 的主要功能 1.1.2 WSL 的版本 1.1.3 为什么使用 WSL&#xff1f; 1.1.4 WSL 的工作原理 1.1.5 WSL 的常见使用场景 1.1.6 与虚拟机的区别 1.1.7 适合使用 WSL 的人群 1.2 启用 WSL 1.2.1 打开 PowerShell&#xff08;管理员模…

【Java树】二叉树遍历的简单实现

二叉树的遍历 二叉树的遍历是值按照一定顺序访问二叉树中所有结点的过程&#xff0c;确保每个结点被访问且仅被访问一次。遍历操作是对二叉树的基础操作&#xff0c;用于后续的查找、排序和路径计算等功能。 二叉树的遍历有以下几种常见方式&#xff1a;深度遍历&#xff08;…

STL算法之set相关算法

STL一共提供了四种与set(集合)相关的算法&#xff0c;分别是并集(union)、交集(intersection)、差集(difference)、对称差集(symmetric difference)。 目录 set_union set_itersection set_difference set_symmetric_difference 所谓set&#xff0c;可细分为数学上定义的和…

鸿蒙ArkUI-X已更新适配API13啦

ArkUI-X 5.0.1 Release版配套OpenHarmony 5.0.1 Rlease&#xff0c;API 13&#xff0c;新增适配部分API 13接口支持跨平台&#xff1b;框架能力进一步完善&#xff0c;支持Android应用非压缩模式&#xff0c;支持Android Fragment对接跨平台。ACE Tools工具易用性提升&#xff…

rest-assured multiPart上传中文名称文件,文件名乱码

rest-assured是一个基于java语言的REST API测试框架&#xff0c;在使用rest-assured的multipart 上传文件后&#xff0c;后端获取的文件名称乱码。截图如下&#xff1a; 原因是rest-assured multipart/form-data默认的编码格式是US-ASCII&#xff0c;需要设置为UTF-8。 Befo…

前端页面或弹窗在线预览文件的N种方式

需求&#xff1a;后端返回给前端一个地址后&#xff0c;在前端页面上或则在弹框中显示在线的文档、表格、图片、pdf、video等等&#xff0c;嵌入到前端页面 方式一&#xff1a; 使用vue-office 地址&#xff1a;vue-office简介 | vue-office 个人感觉这个插件是最好用的&#x…

<<WTF-Solidity>>学习笔记(part 21-24)

part 21: 调用已部署合约 在Solidity中&#xff0c;一个合约可以调用另一个合约的函数&#xff0c;这在构建复杂的DApps时非常有用。本教程将会介绍如何在已知合约代码&#xff08;或接口&#xff09;和地址的情况下&#xff0c;调用已部署的合约。 part 22: Call call 是…

element的el-table表格标题用css自定义是否必填,用添加伪类的方式标红色*

element的el-table表格标题用css自定义是否必填添加伪类红色 * 效果图如下&#x1f447; el-table组件的html部分 css部分 /deep/.el-table__header-wrapper{.el-table__header{.has-gutter tr .el-table__cell:nth-of-type(3) .cell:before{content: *;color:red}.has-gutte…

2024 ccpc 辽宁省赛 E(构造 思维?)L(二分+一点点数论知识?)

E 题意&#xff1a; 可以注意到&#xff1a; 我的两种方格都四个方格的大小。 所以 如果存在一种摆放方式 那么 4|nm。 再考虑一种特殊的情况 22 &#xff0c;此时虽然我的积是4 但是无法摆放的。 1>对于 4 | n,或者 4 | m.我直接摆放第二种方格就可以了。 如果我n 是4 的…

【python】OpenCV—Tracking(10.5)—dlib

文章目录 1、功能描述2、代码实现3、效果展示4、完整代码5、涉及到的库函数dlib.correlation_tracker() 6、参考 1、功能描述 基于 dlib 库&#xff0c;实现指定类别的目标检测和单目标跟踪 2、代码实现 caffe 模型 https://github.com/MediosZ/MobileNet-SSD/tree/master/…

Ps:存储 Adobe PDF

在 Adobe Photoshop 中&#xff0c;将图像保存为 PDF 文件时&#xff0c; 会弹出“存储 Adobe PDF” Save Adobe PDF对话框。在此对话框中提供了多个选项&#xff0c;用于控制 PDF 文件的输出&#xff0c;包括一般设置&#xff08;选择预设、兼容性和保留编辑功能&#xff09;、…