C++三大特性——继承(上篇)

文章目录

目录

一、继承的概念及定义

1.1继承的概念

1.2 继承定义

1.2.1定义格式

1.2.2继承关系和访问限定符

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

二、基类和派生类对象赋值转换

三、继承中的作用域

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


一、继承的概念及定义

1.1继承的概念

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

我们把需要共用的数据和方法提取到一个类中,我们就把这个类叫做父类或基类;我们把继承父类的所有属性和行为,并且可以根据自己需要定义自己的属性和行为的类叫做子类或派生类

#include <iostream>
#include <string>using namespace std;class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}string _name = "peter"; // 姓名int _age = 18; // 年龄
};// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
// 这里体现出了Student和Teacher复用了Person的成员。
// 我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
// 调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:int _stuid; // 学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};int main()
{Student s;s._name = "张三"; s._age = 18;s.Print();Teacher t;t._name = "王老师";t._age = 33;t.Print();return 0;
}

运行结果:


1.2 继承定义

1.2.1定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类,冒号后面的public就是继承方式。

1.2.2继承关系和访问限定符

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

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected
成员
派生类的private
成员
基类的protected
成员
派生类的protected
成员
派生类的protected
成员
派生类的private
成员
基类的private成
在派生类中不可见在派生类中不可见在派生类中不可见

总结:

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

特别注意:protected/private成员 表示在类外面不能访问,在类里面可以访问;而不可见 表示影身,在类的里面和外面都不能访问。

// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:void Print(){cout << _name << endl;}
protected:string _name = "张三"; // 姓名
private:int _age = 18; // 年龄
};//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:int _stunum; // 学号
};int main()
{Student s;s._name = "李四"; // 保护成员遇到共有继承,继承后依然是保护(报错)s._age = 20; // 私有成员遇到共有继承,继承后依然是私有(报错)s.Print(); // 共有成员遇到共有继承,继承后是共有(不报错)return 0;
}

编译器报错内容:


二、基类和派生类对象赋值转换

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割(不能当成隐式类型转换)。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的 dynamic_cast 来进行识别后进行安全转换。

小技巧:

在公有继承中,子类和父类的关系可以看成 is a 的关系,每一个子类对象都是一个特殊的父类对象。

代码示例:

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; // 报错// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;Student * ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_No = 10;return 0;
}


三、继承中的作用域

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

例一

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:string _name = "小黑子"; // 姓名int _num = 111; // 身份证号
};
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl; // 打印Person的_namecout << " 身份证号:" << Person::_num << endl; // 打印Person的_numcout << " 学号:" << _num << endl; // 打印Student的_num}
protected:int _num = 999; // 学号
};int main()
{Student s1;s1.Print();return 0;
}

运行结果:

 例二(易错题)

// 提问:A和B的fun函数构成什么关系class A
{
public:void fun(){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" << i << endl;}
};int main()
{B b;b.fun(10); // 默认访问B中的fun函数,A中的fun函数在B中被隐藏了cout << "-------------" << endl;b.A::fun(); // 想要访问A中的fun函数,就需要指定作用域return 0;
}// 答案:
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。

运行结果:


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

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

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

 调用顺序:

代码示例:

// 父类
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};// 子类
class Student : public Person
{
public:Student(const char* name, int num): Person(name) // 父类没有默认构造,就只能在初始化列表阶段显示调用, _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s) // 赋值转换,子类通过切片传给父类, _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator =(s); // 调用父类的赋值重载时,需要声明作用域,因为父类的赋值重载被隐藏了_num = s._num;}return *this;}~Student(){cout << "~Student()" << endl;}protected:int _num; //学号
};int main()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;return 0;
}

运行结果:

特别注意1:

子类的析构函数跟父类的析构函数会构成隐藏。为什么呢?

由于后面多态的需要,析构函数名会统一处理成destructtor(),这样就导致子类和父类的析构函数名相同,所以就构成了隐藏。

特别注意2:

如果我们把上面子类的析构函数代码改成,显示调用父类析构函数,这时会发生什么呢?

~Student()
{Person::~Person(); // 显示调用父类的析构函数cout << "~Student()" << endl;
}

运行结果:

 这时我们就会发现,明明只创建了3个对象,却调用了6次父类的析构函数,这是为什么呢?

这是因为,在每个子类析构函数后面,编译器会自动调用父类的析构函数,不需要自己显示调用。这样才能保证先析构子类,再析构父类(具体看“特别注意3”)

特别注意3:先定义的对象会后析构,后定义的对象会先析构,因为这些对象是存在栈上的。所以上面的代码是先析构s3再析构s2最后析构s1。所以就有这样的一个规定:子类继承的父类,父类的部分先构造,子类的部分再构造;子类的部分先析构,父类的部分再析构。(这也就是不让你显示调用父类析构函数的原因,因为这样就无法保证析构的顺序了)

总结:其他的父类默认成员函数可以显示的调用,只有父类的析构函数不需要显示调用。

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

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

相关文章

bin-editor-next实现josn序列化

线上链接 BIN-EDITOR-NEXThttps://wangbin3162.gitee.io/bin-editor-next/#/editor gitee地址bin-editor-next: ace-editor 的vue3升级版本https://gitee.com/wangbin3162/bin-editor-next#https://gitee.com/link?targethttps%3A%2F%2Funpkg.com%2Fbin-editor-next%2F 实现…

47 从前序与中序遍历序列构造二叉树

从前序与中序遍历序列构造二叉树 先序无法确定子树大小&#xff0c;中序找不到根&#xff1b;所以用先序找根&#xff0c;用中序找大小题解1 递归题解2 迭代 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同…

Win11自定义目录安装Linux子系统wsl

1. 启用适用于 Linux 的 Windows 子系统和虚拟机功能 以管理员身份打开 PowerShell&#xff08;“开始”菜单 >“PowerShell” >单击右键 >“以管理员身份运行”&#xff09;&#xff0c;然后依次输入执行以下命令&#xff1a; dism.exe /online /enable-feature /f…

vue3+ts项目04-国际化

yarn add vue-i18n yarn add js-cookie yarn add types/js-cookiesrc下新建i18n文件夹&#xff0c;该文件夹下新建lang和pages文件夹&#xff0c; lang文件夹下新建en.ts // 定义内容 export default {router: {home: home,system: {system: system,menu: systemMenu,role: sy…

Dremio:新一代数据湖仓引擎

Dremio数据湖引擎 1、什么是Dremio2、什么是数据湖仓2.1、数据湖仓的历史和演变 3、Dremio查询引擎&#xff08;Dremio Sonar&#xff09;3、Dremio特点1、唯一具有自助式SQL分析功能的数据湖仓2、数据完全开放&#xff0c;无锁定3、亚秒级性能&#xff0c;云数据仓库成本的1/1…

【JavaEE重点知识归纳】第7节:类和对象

目录 一&#xff1a;了解面向对象 1.什么是面向对象 2.面向对象和面向过程区分 二&#xff1a;类定义和使用 1.什么是类 2.练习&#xff1a;定义一个学生类 三&#xff1a;类的实例化 1.什么是实例化 2.类和对象的说明 四&#xff1a;认识this 1.为什么要有this引用…

国内就能使用的chatgpt网页版,包含AIGC应用工具

Chatgpt的出现在多个领域带来了重要的影响。它能够显著提高我们的工作效率&#xff0c;无论是编写文案代码还是回答常见问题&#xff0c;都能在短时间内完成任务。通过Chatgpt&#xff0c;我们能够迅速获取所需答案。随着人工智能技术的不断发展&#xff0c;相信在未来AI能够带…

elasticsearch 8.5.3问题记录

一&#xff1a;解决 elasticsearch 高版本 warning: ignoring JAVA_HOMEC:\Program Files\Java\jdk-11&#xff1b; using bundled JDK if defined JAVA_HOME (set JAVA_HOME%JAVA_HOME%; )示例版本Elasticsearch 8.5.3 可以与 JDK 11 兼容&#xff0c;但不支持 JDK 17。确保选…

Spring Boot中实现发送文本、带附件和HTML邮件

SpringBoot实现发送邮箱 引言 在现代应用程序中&#xff0c;电子邮件通常是不可或缺的一部分。在Spring Boot中&#xff0c;你可以轻松地实现发送不同类型的邮件&#xff0c;包括文本、带附件和HTML邮件。本博客将向你展示如何使用Spring Boot发送这些不同类型的电子邮件。 步…

详细解说iptables 高阶用法,用来完成哪些高效率网络路由策略场景,iptables 实现域名过滤,Linux如何利用iptables屏蔽某些域名?

详细解说iptables 高阶用法,用来完成哪些高效率网络路由策略场景,iptables 实现域名过滤,Linux如何利用iptables屏蔽某些域名? Linux利用iptables屏蔽某些域名 以下规则是屏蔽以 youtube.com 为主的所有一级 二级 三级等域名。 iptables -A OUTPUT -m string --string &qu…

unocss+vite+vue3初使unocss

一、什么是UnoCss&#xff1f; UnoCSS 是一个即时的原子CSS引擎&#xff0c;而非一款框架&#xff0c;因为它并未提供核心工具类&#xff0c;所有功能可以通过预设和内联配置提供。它可以让你用简短的类名来控制元素的样式 原子样式也有很多选择&#xff0c;最著名的就是 Tail…

019 基于Spring Boot的教务管理系统、学生管理系统、课表查询系统

基于Spring Boot的教务管理系统、学生管理系统、课表查询系统 一、系统介绍 本作品主要实现了一个课表查询系统&#xff0c;采用了SSM&#xff08;Spring SpringMVC MyBatis&#xff09;的基础架构。 二、使用技术 spring-bootspring-MVCthymeleafmybatis-plusdruidLombo…

保护 Web 服务器安全性

面向公众的系统&#xff08;如 Web 服务器&#xff09;经常成为攻击者的目标&#xff0c;如果这些业务关键资源没有得到适当的保护&#xff0c;可能会导致安全攻击&#xff0c;从而导致巨大的财务后果&#xff0c;并在客户中失去良好的声誉。 什么是网络服务器审核 当有人想要…

Graph RAG: 知识图谱结合 LLM 的检索增强

本文为大家揭示 NebulaGraph 率先提出的 Graph RAG 方法&#xff0c;这种结合知识图谱、图数据库作为大模型结合私有知识系统的最新技术栈&#xff0c;是 LLM 系列的第三篇&#xff0c;加上之前的图上下文学习、Text2Cypher 这两篇文章&#xff0c;目前 NebulaGraph LLM 相关的…

Folium 笔记:MarkerCluster

在一张地图上以聚簇的形式显示大量的标记&#xff08;markers&#xff09; 举例&#xff1a; import folium from folium.plugins import MarkerCluster import randomm folium.Map(location[45.5236, -122.6750], zoom_start13) # 创建一个基本的地图marker_cluster Marker…

git 取消待推送内容

选择最后一次提交的记录&#xff0c;右键->软合并

k8spod就绪检查失败

pod 一直未就绪 kube-system metrics-server-7764f6c67c-2kts9 0/1 Running 0 10m kubect describe 查看 就绪探针未通过 Normal Started 3m19s kubelet Started container metrics-server Warning Unhealthy 5s (x20 over 2m55s) kubelet Readiness probe failed: HTTP probe…

安全性第一!OpenWRT配置SFTP远程文件传输,实现数据安全保护

文章目录 前言1. openssh-sftp-server 安装2. 安装cpolar工具3.配置SFTP远程访问4.固定远程连接地址 前言 本次教程我们将在OpenWRT上安装SFTP服务&#xff0c;并结合cpolar内网穿透&#xff0c;创建安全隧道映射22端口&#xff0c;实现在公网环境下远程OpenWRT SFTP&#xff…

c++视觉检测-----Canny边缘算子

Canny边缘算子 cv::Canny()是OpenCV库中用于执行Canny边缘检测的函数。Canny边缘检测是一种广泛使用的图像处理技术&#xff0c;用于检测图像中的边缘。 以下是cv::Canny()函数的一般用法和参数&#xff1a; void cv::Canny(cv::InputArray image, // 输入图像&#x…

linux 给根目录扩容(lvm CentOS 7.6 kylinx86)

问题:Linux系统挂载到根目录的磁盘空间满了,如何扩容? 用命令:lsblk 可以查看磁盘和分区情况,可以发现磁盘vda下面的还有大部分空间没有使用。 操作步骤 1、使用 fdisk -l 查看硬盘序号,并用 fdisk 对硬盘操作,格式化成lvm的格式 (用命令lsblk可以看到,挂载到根目录…