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

news2025/1/10 23:35:42

❤️前言

        本篇博客主要是关于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.coloradmin.cn/o/972119.html

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

相关文章

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

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

CentOS7 Hadoop3.3.0 安装与配置

一、安装JDK 1、创建文件夹tools和training用于存放压缩包和解压使用&#xff0c;tools存放压缩包&#xff0c;training用于解压后安装jdk和hadoop的路径。 1&#xff09;回到路径为 / 的位置 cd /2) 创建 tools 和 training mkdir toolsmkdir training3) 进入tools文件夹 …

47、TCP的流量控制

从这一节开始&#xff0c;我们学习通信双方应用进程建立TCP连接之后&#xff0c;数据传输过程中&#xff0c;TCP有哪些机制保证传输可靠性的。本节先学习第一种机制&#xff1a;流量控制。 窗口与流量控制 首先&#xff0c;我们要知道的是&#xff1a;什么是流量控制&#xff…

AI篇-chatgpt基本用法(文心一言也适用)

目录 &#xff08;1&#xff09;基本规则 &#xff08;2&#xff09;例子1-文章摘要 &#xff08;3&#xff09;例子2-代码生成 &#xff08;4&#xff09;文心一言链接 &#xff08;1&#xff09;基本规则 相比于搜索引擎&#xff0c;ChatGPT的优势在于其高效的想法关联和…

Passwork 和 KeePass 密码管理器对比和选择

互联网蓬勃发展的今天&#xff0c;基本上我们能够想到的功能都可以找到相似的软件辅助工作效率。包括我们日常用到比较多的各种网站、软件账户的管理&#xff0c;如果用密码管理器管理可以确保信息的安全以及提高密码的安全性。在前面的文章中&#xff0c;乐小虎有写过”横向评…

ES6中导入import导出export

ES6使用 export 和 import 来导出、导入模块 用法 /** 导出 export *///分别导出 export let name 孙悟空; export function sum(a, b) {return a b; } } //先定义再导出 let age 18 export {age}/** 默认导出 export default */const a 默认导出; export default a;/**…

CS420 课程笔记 P4 - 以16进制形态编辑游戏文件

文章目录 IntroductionFinding save filesStringsUnicodeExample!Value searchHealth searchConclusion Introduction 这节课我们将学习编辑十六进制&#xff0c;主要用于编辑保存文件&#xff0c;但十六进制编辑涉及的技能可以很好地转移到&#xff1a; Save file editingRe…

CG MAGIC分享3d Max中的Corona渲染器材质如何成转换VRay材质?

大家无论是使用Corona渲染器还是Vray渲染器时&#xff0c;进行材质问题时&#xff0c;都会遇到转化材质问题。 如何将CR转换成VR或者将VR转换CR材质呢&#xff1f; 对于这两者之间转换最好最好的方法只能是材质转换器。 CG MAGIC小编&#xff0c;梳理了两种方法&#xff0c;大…

python-wordcloud词云

导入模块 from wordcloud import WordCloud import jieba import imageio import matplotlib.pyplot as plt from PIL import ImageGrab import numpy as npwordcloud以空格为分隔符号&#xff0c;来将文本分隔成单词 PIL pillow模块 img imageio.imread(image.png)这行代码…

ToBeWritten之ATTCK实践的常见误区与最佳实践

也许每个人出生的时候都以为这世界都是为他一个人而存在的&#xff0c;当他发现自己错的时候&#xff0c;他便开始长大 少走了弯路&#xff0c;也就错过了风景&#xff0c;无论如何&#xff0c;感谢经历 转移发布平台通知&#xff1a;将不再在CSDN博客发布新文章&#xff0c;敬…

阿里云大数据实战记录9:MaxCompute RAM 用户与授权

文章目录 问题来源&#xff1a;maxcompute 管理员无法访问敏感列&#xff1f;主线问题&#xff1a;如何提高用户等级衍生问题1&#xff1a;怎么知道自己的等级和表单的等级衍生问题2&#xff1a;为什么 dataworks 空间管理员也没有设置等级的权限&#xff1f;衍生问题3&#xf…

通过IP地址和子网掩码与运算计算相关地址

通过IP地址和子网掩码与运算计算相关地址 知道ip地址和子网掩码后可以算出&#xff1a; 1、 网络地址 2、 广播地址 3、 地址范围 4、 本网有几台主机 例1&#xff1a;下面例子IP地址为1921681005 子网掩码是2552552550。算出网络地址、广播地址、地址范围、主机数。 一&…

Python 操作 Excel

之前看过一篇文章&#xff0c;说一个工作多年的老员工&#xff0c;处理数据时只会用复制粘贴到 Excel &#xff0c;天天加班工作还完不成&#xff0c;后来公司就招了一个会 Python 的新人&#xff0c;结果分分钟就处理完成。所以工作中大家经常会使用 Excel 去处理以及展示数据…

SSRF漏洞复现(redis)

文章目录 启动环境漏洞复现探测存活IP和端口服务计划任务反弹shell 前提条件&#xff1a; 1.安装docker docker pull medicean/vulapps:j_joomla_22.安装docker-compose docker run -d -p 8000:80 medicean/vulapps:j_joomla_23.下载vulhub 安装环境已完成&#xff0c;故此省略…

C头文件只引用一次的方法

我们自己在写头文件的时候&#xff0c;如果不加入一些独特的方法&#xff0c;有可能造成重复引用的可能&#xff0c;造成代码的冗余&#xff0c;占用空间大&#xff0c;降低效率。所以要保证只引用一次就变的非常重要&#xff0c;此处介绍两种方法&#xff1a; 1、#pragma onc…

窗口函数-分组排序:row_number()、rank() 、dense_rank()、ntile()

窗口函数语法结构&#xff1a; 分析函数() over(partition by 分组列名 order by 排序列名 rows between 开始位置 and 结束位置) 开窗函数和聚合函数区别&#xff1a; 聚合函数会对一组值进行计算并返回一个值&#xff0c;常见的比如sum()&#xff0c;count()&#xff0c;ma…

在Cisco设备上配置接口速度和双工

默认情况下&#xff0c;思科交换机将自动协商速度和双工设置。将设备&#xff08;交换机、路由器或工作站&#xff09;连接到 Cisco 交换机上的端口时&#xff0c;将发生协商过程&#xff0c;设备将就传输参数达成一致&#xff0c;当今的大多数网络适配器都支持此功能。 在本文…

第三章 USB应用笔记之USB鼠标(以STM32 hal库为例)

第三章 USB应用笔记之USB鼠标&#xff08;以STM32 hal库为例&#xff09; 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 第三章 USB应用笔记之USB鼠标&#xff08;以STM32 hal库为例&#xff09;前言一、STM32 U…

连接全球金融网络 探索SCF公链的多元价值

在当今数字时代&#xff0c;区块链技术正迅速演变成为驱动各行业创新的重要引擎。公链作为区块链技术的核心之一&#xff0c;正在逐步展现其在金融和技术领域的巨大潜力。与此同时&#xff0c;SCF金融公链作为这一领域的新秀&#xff0c;正以其独特的优势和前瞻性的技术构建起一…

Rasa 多轮对话机器人

Rasa 开源机器人 目录 Rasa 开源机器人 1. 学习资料 2. Rasa 安装 2.1. rasa 简介 2.2. Rasa系统结构 ​编辑 2.3. 项目的基本流程 ​编辑 2.4. Rasa安装 2.5. 组件介绍 3. Rasa NLU 3.0. NLU 推理输出格式 3.1. 训练数据 ./data/nlu.yml 数据文件 3.2. ./confi…