【C++从0到王者】第二十一站:继承

news/2024/4/29 21:09:00/文章来源:https://blog.csdn.net/jhdhdhehej/article/details/132259992

文章目录

  • 前言
  • 一、继承的概念及定义
    • 1. 继承的概念
    • 2.继承的格式
    • 3.继承关系与访问限定符
  • 二、基类和派生类的赋值转换
  • 三、继承中的作用域
  • 四、派生类的默认成员函数
  • 五、继承与友元
  • 六、继承与静态成员


前言

继承是面向对象的三大特性之一。我们常常会遇到这样的情况。很多角色的信息是十分类似的,他们有公共的信息,还有独有的信息。比如学生、老师、保安大叔、食堂阿姨等。他们都有一份公有的信息。如果将这些接口给重复写很多次,是非常麻烦的。

class student
{string name;int age;string address;int tel;//其他独有信息//宿舍号、学号、专业...
};
class teacher
{string name;int age;string address;int tel;//其他独有信息//工号、学院、职称...
};

基于以上的原因我们引出了继承。即将公有的信息全部单独做好,然后让其他身份可以直接使用这个类,即继承了这个类

class Person
{string name;int age;string address;int tel;
};

一、继承的概念及定义

1. 继承的概念

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

通俗的讲:继承的本质就是复用,不过这里是类层面的复用,包括成员变量和成员函数

2.继承的格式

如下图所示,继承的格式即在定义新的类的时候,在后面加上冒号,继承方式和基类。
Person是父类,也被称之为基类。Student是子类,也被称之为派生类
在这里插入图片描述

如下是一个简单的继承,其中,Stundet和Teacher继承了Person。

class Person
{
public:void Print(){cout << "name :" << _name << endl;cout << "age :" << _age << endl;}
protected:string _name = "zhangsan";int _age = 18;
};
class Student : public Person
{
protected:int _stuid; //学号
};
class Teacher : public Person
{
protected :int _jobid; //工号
};
int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}

我们在监视窗口看到的样子是这样的

可以观测到,继承即直接在s或t中有了Person这样一个类。拥有它的成员变量和成员函数。注意这里的拥有的成员函数指的是可以去调用它的成员函数,因为在类里面本身成员函数就是放在一个公共区域的。所以这里调用的成员函数其实还是公共区域的成员函数
在这里插入图片描述代码运行结果如下所示
在这里插入图片描述

3.继承关系与访问限定符

我们知道访问限定符有三种,public,protected,private三种。同样的继承方式也是一样的。
在这里插入图片描述

那么这些又有何关系呢?

如下表所示,是继承基类成员访问的变化

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

对于这个表,我们需要知道的是

  1. 基类的私有成员,在派生类中是不可见的。这里的不可见指的是在派生类中有,但是没法用(类里面和类外面都不能用)。即语法上限制了访问,但是在内存中是存在的。与private是不一样的, private是类内可以使用,类外不可使用。

下面是一个样例,即父类私有成员,子类无论如何都用不了
在这里插入图片描述

  1. 对于公有和保护的,他们的关系其实就是取小的那一个,关系是public > protected >private。即public继承后,原来是什么成员,派生类还是什么成员。protected继承,原来是public还是protected成员现在都变成了protected成员。将原来的公有都变成了只在类里面可以使用的成员,但是这些成员还是可以被再次继承的并使用的。而private继承的话,原来无论是什么成员现在一律变为private成员,只能在派生类中适合用,而且如果别人在继承这个子类的话,那么新的派生类是无法访问这个成员的。
    所以我们就知道了,protected和private这两个访问限定符的区别。在之前他们还是一样的,但是现在,在继承中,他们有了区别,如果基类成员不想被类外的访问,但是在派生类中可以被访问的话,那么就使用protected。可以看出保护成员限定符是因为继承而出现的

  2. 还有一点需要注意的是,默认继承。class是private继承,struct是public继承,但是我们最好写出他们的继承方式。

在这里插入图片描述

  1. 事实上,我们常用的就是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
    我们一般使用的就是如下两种场景
    在这里插入图片描述

二、基类和派生类的赋值转换

在我们正常的两个不同类型的对象进行赋值的时候一般是不允许的操作。如果真的允许了,那也是通过类型转换实现的

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "zhangsan";int _age = 18;
};
class Student : public Person
{
protected:int _stuid;
};
class Teacher : public Person
{
protected:int _jobid;
};
int main()
{int i = 0;double d = i; //发生了类型转换Person p;Student s;p = s;//s = p; 不允许的操作return 0;
}

在继承中,也是存在类似于类型转换的。
在赋值的过程中,子可以给父,但是父不可以给子
至于原因也是很简单的。因为派生类中有些成员基类就没有,而基类的所有成员派生类都有,所以有了子可以给父的赋值
在这里插入图片描述

这里的父不可以给子是很严格的,即便使用了强制类型转换,依然报错。语法上直接给禁掉了。
在这里插入图片描述

一般我们也将子赋值给父称之向上转换,这样做是可以的。而向下转换,即父对象赋值给子对象是不允许的
在这里插入图片描述

这里的赋值转换和普通的赋值还是有一些不一样的。在我们之前的不同类型的赋值中,都要走一个隐式类型转换、强制类型转换等。这些都会产生临时变量。而这里是不会产生临时变量的。这里发生了一个特殊处理,即赋值兼容转换(或切割、切片)

这个赋值兼容(切割、切片)是天然的,不会产生临时变量。它不像以前一样不同类型转换会产生临时变量。

这里的切割切片就是认为每一个子类对象都是一个特殊的父类对象,它会将属于父类的一部分切出来进行赋值,然后将它拷贝给父类,所以称为切片。

那么如何证明没有临时变量呢?
如下代码所示就可以进行证明。如果中间产生了临时变量,那么我们使用引用的时候必须加上const进行修饰,因为临时变量具有常性。而我们父类引用子类的时候却没有加上const也不报错,故中间一定没有产生临时变量。而且我们还得出了,引用也可以向上转换
在这里插入图片描述

在上面代码中,经过引用以后p1就变成了s中父类部分的别名。我们先将 Person中的成员变量改为公有,然后使用p1这个别名进行修改,可以看到s也被修改了。从而印证了子类的别名也是可以给父类的。父类可以去引用子类。
在这里插入图片描述

除了引用之外,还有指针也是可以通过向上转换的。
在这里插入图片描述

现在我们就知道了对于向上转换而言,子类对象给父类对象,父类引用子类,父类指针指向子类都是可以的。
而对于向下转换,首先父类对象给子类对象是绝对不可以的,那么子类引用父类,子类指针指向父类呢?其实是可以的。不过这里稍微有点复杂,我们在后面文章在详细探讨。

三、继承中的作用域

我们知道,定义了一个类,这个类就有它自己的类域。对于派生类和基类都有它们自己的类域。

对于父类和子类,是允许有同名成员的。语法上是没有任何问题的。
但是当父类和子类出现同名成员的时候,优先使用子类的成员,如果子类没有,才去父类找。

如下代码所示:

class Person
{
protected:string _name = "zhangsan";int _age = 18;int _num = 666;
};
class Student : public Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;cout << "num:" << _num << endl;}
protected:int _stuid;int _num = 111;
};
int main()
{Student s;s.Print();return 0;
}

可以看到最终结果是111.
在这里插入图片描述

但是如果就想访问父类的也是可以的,我们使用域作用限定符即可。

在这里插入图片描述

而编译器这样的操作,我们也称之为:隐藏/重定义,即子类和父类有同名成员,默认子类的成员隐藏了父类的成员

同样的,对于成员函数,我们也是同样的道理,默认访问子类的成员函数,但是如果使用域作用限定符,也是可以访问到父类的函数的。

不仅仅对于成员变量存在隐藏,对于成员函数也是存在隐藏的。规则与前面是一样的

class Person
{
public:void func(){cout << "Person::func()" << endl;}
protected:string _name = "zhangsan";int _age = 18;int _num = 666;
};
class Student : public Person
{
public:void func(){cout << "Student::func()" << endl;}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;cout << "num:" << Person::_num << endl;}
protected:int _stuid;int _num = 111;
};
int main()
{Student s;s.func();s.Person::func();return 0;
}

在这里插入图片描述

我们如果对上面的代码稍作修改

即,在下面这种情况下,两个func构成什么关系?
a.隐藏/重定义 b.重载 c.重写/覆盖 d.编译报错

class Person
{
public:void func(){cout << "Person::func()" << endl;}
protected:string _name = "zhangsan";int _age = 18;int _num = 666;
};
class Student : public Person
{
public:void func(int i){cout << "Student::func(i)" << endl;}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;cout << "num:" << Person::_num << endl;}
protected:int _stuid;int _num = 111;
};

这道题答案是选a的,我们很容易误选为b,事实上重载的前提条件是在同一个作用域,这两个并不在同一个作用域,所以肯定不是重载。

如下面的测试,只要函数名相同就会构成隐藏,不会考虑到参数这些问题(因为函数名修饰规则在链接阶段)。中间的会在编译阶段就已经报错了。编译阶段带参的隐藏了无参的。所以最终中间的代码会报错在这里插入图片描述

注意:在实际中的继承体系里最好不要定义同名成员

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

我们知道类有六个默认成员函数。“默认”的意思就是指我们不写,编译器会变我们自动生成一个。那么在派生类中,它们的生成又是如何进行变化的呢?

我们将下面这个类作为父类

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; // 姓名
};

然后当我们对派生类写它的构造函数的时候,我们传统的理解为_name可以直接使用,于是我们直接对_name放在了初始化列表中进行初始化。但是很遗憾,报错了。
在这里插入图片描述

初此之外,当我们决定先不管这个变量的时候,我们会发现编译器自动调用了父类的构造和析构
在这里插入图片描述

这是为什么呢?其实是因为C++规定了派生类必须调用父类的构造函数进行初始化。
而且这里的调用是在初始化列表调用的且调用的是默认构造,如果不提供默认构造也会报错。
在这里插入图片描述

这里其实就有点类似于将父类当成一个自定义类型的成员进行处理了。

相当于这里其实就分的很清楚,父类的交给父类的构造函数去搞。子类的自己去搞

而这里如果我们要自己去调用构造函数的话,我们就要像定义一个匿名对象一样在初始化列表中
在这里插入图片描述

而在初始化列表中,永远也是父类的第一个进行执行。相当于它永远是第一个成员变量。

以上是针对于构造函数的分析。
下面是针对拷贝构造的分析
当我们想要写一个拷贝构造的时候,拷贝构造本质也是一个构造函数,所以也要写初始化列表
在这里插入图片描述

如上所示,我们这里对于Person要显式调用它的拷贝构造函数,这里虽然我们没有父类对象,但是由于前面说了,可以向上转换,所以直接将s传过去就可以了。所以下面会被初始化为zhangsan
在这里插入图片描述

这里如果我们不显式写拷贝构造的化也是没问题的。不写,它就初始化列表自动调用默认构造函数(因为拷贝构造也是一个构造函数要遵循构造函数的规则),所以下面会被初始化为peter。

在这里插入图片描述

还有一个默认成员函数是赋值运算符重载,我们不难写出这样的代码,注意这里必须指定父类中的赋值运算符重载,才能将父类的成员函数给赋值过去。然后再来一个普通的赋值即可。如果不指定父类的话,就是默认找子类的,就会发生无穷递归,栈溢出了。
在这里插入图片描述

下面是析构函数
如下所示,是我们想象中析构函数应该有的样子。注意,这里也必须加上父类的访问限定符,虽然看上去好像可以直接调用,但是必须加上,因为不加会报错,报错是因为由于多态的原因,析构函数的函数名被特殊处理了,统一处理为destructor
在这里插入图片描述

但是上面仅仅是我们所想的,实际上上面是错误的。因为如下图所示,我们会发现Person被析构的次数多了一倍。
在这里插入图片描述

而一旦我们显示调用的析构给屏蔽掉,就正确了
在这里插入图片描述

所以说,析构函数不需要我们自己去调用。因为它必须要保证析构顺序,默认是最后才析构的(构造顺序是,先父后子,析构顺序是先子后父),为了保证这个顺序,于是编译器始终默认最后才自动调用析构函数。而如果让我们显式调用的话,是没法保证先子后父的。而必须先析构子在析构父的一个原因就是子可以用父,父不能用子。也就是说,如果先析构了父的话,但是如果后面子突然调用了父的一部分成员,就会出错了。

五、继承与友元

一个核心:友元关系不可以被继承

如下代码所示:我们先声明了Student类,然后我们用Student继承Person类,Display函数是Person的友元。所以在Display函数中可以去访问Person类成员变量,但是这个友元关系不可以被继承,所以Display中直接访问Student成员变量直接报错
在这里插入图片描述

如果要让这个函数可以访问子类,那么可以对子类也使用友元


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

在这里插入图片描述

六、继承与静态成员

静态成员能否被继承呢?
其实: 基类定义了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; // 学号
};int main()
{Person p;Student s;cout << Person::_count << endl;cout << &p._name << endl;cout << &s._name << endl;cout << &p._count<< endl;cout << &s._count << endl;cout << &Person::_count << endl;cout << &Student::_count << endl;return 0;
}

在这里插入图片描述


好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!

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

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

相关文章

MYSQL 作业三

创建一个student表格&#xff1a; create table student( id int(10) not null unique primary key, name varchar(20) not null, sex varchar(4), birth year, department varchar(20), address varchar(50) ); 创建一个score表格 create table score( id int(10) n…

ASP.NET WEB API通过SugarSql连接MySQL数据库

注意&#xff1a;VS2022企业版可以&#xff0c;社区版可能存在问题。实体名称和字段和数据库中的要一致。 1、创建项目&#xff0c;安装SqlSugarCore、Pomelo.EntityFrameworkCore.MySql插件 2、文件结构 2、appsettings.json { “Logging”: { “LogLevel”: { “Default”: …

从零实现kv存储V2.0

在V1.0版本&#xff0c;我们实现了基于array的kv存储引擎。本文继续完善&#xff0c;增加rbtree、hash、skiptable引擎。 实际上&#xff0c;在框架确定的基础上&#xff0c;其他的引擎只需要添加接口即可。 一、架构设计 二、具体实现 2.1 引擎层 //---------------------…

每天一道leetcode:646. 最长数对链(动态规划中等)

今日份题目&#xff1a; 给你一个由 n 个数对组成的数对数组 pairs &#xff0c;其中 pairs[i] [lefti, righti] 且 lefti < righti 。 现在&#xff0c;我们定义一种 跟随 关系&#xff0c;当且仅当 b < c 时&#xff0c;数对 p2 [c, d] 才可以跟在 p1 [a, b] 后面…

WSL2 Ubuntu子系统安装OpenCV

文章目录 前言一、&#xfeff;基本概念二、操作步骤1.下载源码2.安装依赖3.运行编译4.配置路径 前言 OpenCV用C语言编写&#xff0c;它的主要接口也是C语言&#xff0c;但是依然保留了大量的C语言接口。该库也有大量的Python, Java and MATLAB/OCTAVE (版本2.5)的接口。这些语…

最长递增子序列——力扣300

int lengthOfLIS(vector<int>& nums) {int len=1, n=nums.size();if

临床试验三原则-对照、重复、随机

临床试验必须遵循三个基本原则&#xff1a;对照、重复、随机。 一、对照原则和对照的设置 核心观点&#xff1a;有比较才有鉴别。 对照组和试验组同质可比。 三臂试验 安慰剂&#xff1a;试验组&#xff1a;阳性对照组1&#xff1a;n&#xff1a;m&#xff08;n≥m&#xff…

Android Framework 动态更新插拔设备节点执行权限

TF卡设备节点是插上之后动态添加&#xff0c;所以不能通过初始化设备节点权限来解决&#xff0c;需要监听TF插入事件&#xff0c;在init.rc 监听插入后动态更新设备节点执行权限 添加插拔TF卡监听 frameworks/base/services/core/java/com/android/server/StorageManagerServic…

轻量级自动化测试框架WebZ

一、什么是WebZ WebZ是我用Python写的“关键字驱动”的自动化测试框架&#xff0c;基于WebDriver。 设计该框架的初衷是&#xff1a;用自动化测试让测试人员从一些简单却重复的测试中解放出来。之所以用“关键字驱动”模式是因为我觉得这样能让测试人员&#xff08;测试执行人员…

《Java-SE-第三十八章》之注解

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

Hlang社区项目说明

文章目录 前言Hlang社区技术前端后端 前言 Hello,欢迎来到本专栏&#xff0c;那么这也是第一次做这种类型的专栏&#xff0c;如有不做多多指教。那么在这里我要隆重介绍的就是这个Hlang这个项目。 首先&#xff0c;这里我要说明的是&#xff0c;我们的这个项目其实是分为两个…

Docker容器:docker基础概述、安装、网络及资源控制

文章目录 一.docker容器概述1.什么是容器2. docker与虚拟机的区别2.1 docker虚拟化产品有哪些及其对比2.2 Docker与虚拟机的区别 3.Docker容器的使用场景4.Docker容器的优点5.Docker 的底层运行原理6.namespace的六项隔离7.Docker核心概念 二.Docker安装 及管理1.安装 Docker1.…

Python语法基础——循环

学习目标 通过使用while循环编写重复执行的语句。遵从循环的设计策略开发循环。利用用户的确认控制循环。用哨兵值控制循环。通过使用输入重定向从文件获取大量数据而不是从键盘输入来来获取大量数据&#xff0c;并且使用输出重定向将输出存人文件。使用for循环来实现计数器控制…

vue 发现页面找不到3秒后跳转到本页面

这个路由跳转用到的是编程式跳转this.$router.push 两种写法&#xff1a; 第一种可以通过path来跳转 goto(/find) find是路由里边的路径 <span click"goto(/find)">发现音乐</span> <span click"goto(/my)">我的音乐</span> <…

单片机如何分散加载文件

本篇文章将通过实际操作介绍如何实现分散加载文件的方法。开发工具为&#xff1a;mdk&#xff1b;开发板&#xff1a;野火stm32f407 一、建立工程 通过实现简单的加法计算的软件算法&#xff0c;来了解分散加载image 的方法。 建立工程&#xff0c;创建文件夹以及相应的文件&am…

【boost网络库从青铜到王者】第三篇:asio网络编程中的buffer缓存数据结构

文章目录 1、关于buffer数据结构1.1、简单概括一下&#xff0c;我们可以用buffer() 函数生成我们要用的缓存存储数据。1.2、但是这太复杂了&#xff0c;可以直接用buffer函数转化为send需要的参数类型:1.3、output_buf可以直接传递给该send接口。我们也可以将数组转化为send接受…

PyQt5资源的加载和使用,即如何使用Pyrcc

1、打开QtDesigner&#xff0c;选择编辑资源 2、新建资源文件&#xff0c;随便找个地方保存 3、按照自己的喜好命名&#xff0c;然后添加资源 4、保存并退出 5、我们创建一个QLabel&#xff0c;在这里添加资源 6、我们保存界面文件&#xff0c;并编译为py文件&#xff0c;然后…

SpringBoot + Mybatis多数据源

一、配置文件 spring: # datasource: # username: root # password: 123456 # url: jdbc:mysql://127.0.0.1:3306/jun01?characterEncodingutf-8&serverTimezoneUTC # driver-class-name: com.mysql.cj.jdbc.Driverdatasource:# 数据源1onedata:jdbc-url: j…

【5款登录验证校验】基于jquery实现的5款登录验证码组件(附完整源码)

文章目录 写在前面涉及知识点1、随机字母验证码1.1 效果1.2 实现源码 2、数字运算验证码2.1 效果2.2 实现源码 3、滑块验证码3.1 效果3.2 实现源码 4、图片补全验证码4.1 效果4.2 实现源码 5、顺序点选验证码5.1 效果5.2 实现源码 6、源码分享6.1 百度网盘6.2 123网盘6.3 邮箱留…

基于STM32的超声波雷达

视频地址:1.基于STM32的超声波雷达-演示_哔哩哔哩_bilibili 备注:文档最后有所有文件的网盘地址 1. 项目展示 1.1. 最终效果 1.2. 项目硬件 全部必要硬件(左到右): ST-LINK调试器:调试下载程序stm32f103c8t6核心板1.8寸TFT显示器sg90舵机超声波传感器