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

news2025/1/22 14:41:47

文章目录

  • 前言
  • 一、继承的概念及定义
    • 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.coloradmin.cn/o/883580.html

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

相关文章

显卡nvidia-smi后 提示Faild 解决过程,包含卸载重装NVIDIA驱动步骤

显卡异常: 显卡nvidia-smi后 提示Faild 解决过程&#xff0c;卸载重装nvidia驱动步骤 文章目录 显卡异常: 显卡nvidia-smi后 提示Faild 解决过程&#xff0c;卸载重装nvidia驱动步骤 [toc]1 缘由2 解决过程3 过程所需命令4 解决4.1 把该显卡重新拔插一下卸载NVIDIA驱动的方法&a…

远程遥控IPTables进行端口复用

一、配置&#xff08;通过ip进行ping&#xff09; 1.创建复用链 iptables -t nat -N LETMEIN 2.创建端口复用将流量转发到22端口上 iptables -t nat -A LETMEIN -p tcp -j REDIRECT --to-port 22 3.开启开关&#xff0c;如果接收到一个长为 1139 的 ICMP 包&#xff0c;则将…

Ajax及前端工程化

Ajax&#xff1a;异步的js与xml。 作用&#xff1a; 1、通过ajax给服务器发送数据&#xff0c;并获得其响应的数据。 2、可以在不更新整个网页的情况下&#xff0c;与服务器交换数据并更新部分网页的技术。 一、同步与异步 二、原生Ajax 1、准备数据地址 2、创建XMLHttpReq…

图神经网络 day2 图的分类

图神经网络基础算法 1 GCN2 GraphSAGE2.1 采样&#xff1a;采样固定长度的邻居2.2 聚合2.3 GraphSAGE_minibatch2.4 GraphSAGE_embedding 3 GAT4. 图网络的分类4.1 递归图神经网络 RGNN4.2 图卷积神经网络GCN4.3 图注意力网络 GAT4.4 图自动编码 GAE4.5 图时空网络 GSTN4.6 图生…

echarts 柱状图-折线图-饼图的基础使用

上图示例图表展示相关配置&#xff1a; var myChart echarts.init(this.$refs.firstMain);myChart.setOption({legend: { // 图例设置top: "15%",type: "scroll",orient: "vertical",//图例列表的布局朝向。left: "right",pageIconCo…

【Docker】 Docker-Composite 启动 WordPress

引 本文将使用流行的博客搭建工具 WordPress 搭建一个私人博客站点。部署过程中使用到了 Docker 、MySQL 。站点搭建完成后经行了发布文章的体验。 WordPress WordPress 是一个广泛使用的开源内容管理系统&#xff08;CMS&#xff09;&#xff0c;用于构建和管理网站、博客和…

ChatGPT​保密吗?它有哪些潜在风险?如何规避?

自2022年11月公开发布以来&#xff0c;ChatGPT已成为许多企业和个人的必备工具&#xff0c;但随着该技术越来越多地融入我们的日常生活&#xff0c;人们很自然地想知道&#xff1a;ChatGPT是否是保密的。 问&#xff1a;ChatGPT保密吗&#xff1f; 答&#xff1a;否&#xff0…

MIUI免费字体更换

一、打开主题壁纸 二、选择 热销字 三、点击右上角 搜索 四、输入 字体 可以看到&#xff0c;免费的字体没多少&#xff0c;此时这里可以输入其他关键词&#xff1a;拼音、手写等&#xff0c;看个人需求进行筛选免费即可 关键词有以下这些&#xff0c;但不局限这些哈 五、点击…

linux系统服务学习(六)FTP服务学习

文章目录 FTP、NFS、SAMBA系统服务一、FTP服务概述1、FTP服务介绍2、FTP服务的客户端工具3、FTP的两种运行模式&#xff08;了解&#xff09;☆ 主动模式☆ 被动模式 4、搭建FTP服务&#xff08;重要&#xff09;5、FTP的配置文件详解&#xff08;重要&#xff09; 二、FTP任务…

共读《科研论文配图绘制指南--基于Python》学习重点

Book 《科研论文配图绘制指南–基于Python》 特别提示 学习内容&#xff08;书籍前3章&#xff09;开营时在群内以PDF形式发放 课程背景 系统地介绍基于Python的科研论文配图的绘制技巧&#xff0c;提高科研工作者的绘图效率&#xff1b; 100多种图形的详细绘制方法&#…

STM32F103-OLED使用教程

目录 1. OLED屏介绍2. OLED如何显示一个点3. 配置OLED屏幕4. OLED显示字符串和汉字5. OLED屏幕显示图片6. 总结 1. OLED屏介绍 OLED&#xff08;Organic Light Emitting Diode&#xff09;&#xff1a;有机发光二极管OLED显示屏&#xff1a;性能优异的新型显示屏&#xff0c;具…

Vue组件(详解)

目录 组件&#xff1a; 全局组件&#xff1a; 在HTML页面声明template&#xff1a; 局部组件&#xff1a; 局部组件第一种方式&#xff1a; 局部组件第二种方式&#xff1a; 插槽slot&#xff1a; 匿名插槽&#xff1a; 具名插槽&#xff1a; 父子组件通信&#xff1…

【双指针_和为 s 的两个数_C++】

和为s的两个数字 class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {int n nums.size();int left 0;int right n-1;while(left<right){if(nums[left]nums[right]>target) right--;else if(nums[left]nums[right]<tar…

Postman接口自动化测试实例

一.实例背景 在实际业务中&#xff0c;经常会出现让用户输入用户密码进行验证的场景。而为了安全&#xff0c;一般都会先请求后台服务器获取一个随机数做为盐值&#xff0c;然后将盐值和用户输入的密码通过前端的加密算法生成加密后串传给后台服务器&#xff0c;后台服务器接到…

车载以太网物理层

车载以太网物理层 O S I 参考模型的第 1 层&#xff08; 最底层&#xff09;。负责逻辑信号&#xff08; 比特流&#xff09;与物理信号&#xff08;电信号、光信号&#xff09;之间的互相转换&#xff0c;通过传输介质为数据链路层提供物理连接。 车载以太网与传统以太网相比…

matlab使用教程(16)—图论中图的定义与修改

1.修改现有图的节点和边 此示例演示如何使用 addedge 、 rmedge 、 addnode 、 rmnode 、 findedge 、 findnode 及 subgraph 函数访问和修改 graph 或 digraph 对象中的节点和/或边。 1.1 添加节点 创建一个包含四个节点和四条边的图。s 和 t 中的对应元素用于指定每条…

【教程】零成本将小米净化器改造为无叶风扇

某宝某多上&#xff0c;就这么点破塑料&#xff0c;就要买79&#xff1f;&#xff01;&#xff01; 我这枚韭菜可不上当。咱自己做一个&#xff01; 真香~

BBS项目day02、注册、登录(登录之随机验证码)、修改密码、退出登录、密码加密加盐

一、注册 1.注册之前端页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>注册页面</title><!--动态引入文件-->{% load static %}<script src"{% static js/jquery.min.js %…

jquery技术学习2

移动节点 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>移动节点</title><script type"text/javascript" src"../script/jquery-3.6.0.min.js"></script>&l…

【Spring】深入理解 Spring 事务及其传播机制

文章目录 一、Spring 事务是什么二、Spring 中事务的实现方法2.1 Spring 编程式事务&#xff08;手动&#xff09;2.1.1 编程式事务的使用演示2.1.2 编程式事务存在的问题 2.2 Spring 声明式事务&#xff08;自动&#xff09;2.2.1 Transactional 作用范围2.2.2 Transactional …