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

文章目录

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

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

相关文章

一、docker及mysql基本语法

文章目录 一、docker相关命令二、mysql相关命令 一、docker相关命令 &#xff08;1&#xff09;拉取镜像&#xff1a;docker pull <镜像ID/image> &#xff08;2&#xff09;查看当前docker中的镜像&#xff1a;docker images &#xff08;3&#xff09;删除镜像&#x…

Python web实战之细说 Django 的单元测试

关键词&#xff1a; Python Web 开发、Django、单元测试、测试驱动开发、TDD、测试框架、持续集成、自动化测试 大家好&#xff0c;今天&#xff0c;我将带领大家进入 Python Web 开发的新世界&#xff0c;深入探讨 Django 的单元测试。通过本文的实战案例和详细讲解&#xff…

DNS域名解析服务器

一、DNS简介 1、因特网的域名结构 2、域名服务器的类型划分 二、DNS域名解析的过程 三、DNS服务器配置 两个都定义&#xff0c;ttl的优先&#xff1a; 能解析&#xff0c;不能拼通&#xff08;没有13这个主机&#xff09; 别名&#xff1a; 测试&#xff1a; 主&#xff08;192…

AI在日常生活中的应用:从语音助手到自动驾驶

文章目录 AI的定义和发展AI在日常生活中的应用1. **智能语音助手**2. **智能家居**3. **智能医疗**4. **自动驾驶** 代码示例&#xff1a;使用Python实现基于机器学习的图片分类AI的未来前景结论 &#x1f389;欢迎来到AIGC人工智能专栏~探索AI在日常生活中的应用 ☆* o(≧▽≦…

fiddler抓包问题记录,支持https、解决 tunnel to 443

fiddler下载安装步骤及基本配置 fiddler抓包教程&#xff0c;如何抓取HTTPS请求&#xff0c;详细教程 可能遇到的问题及解决方案 1. 不能正常访问页面&#xff08;所有https都无法访问&#xff09; 解决方案&#xff1a;查看下面配置是否正确 Rules-customization 找到 OnB…

Django进阶:DRF(Django REST framework)

什么是DRF&#xff1f; DRF即Django REST framework的缩写&#xff0c;官网上说&#xff1a;Django REST framework是一个强大而灵活的工具包&#xff0c;用于构建Web API。 简单来说&#xff1a;通过DRF创建API后&#xff0c;就可以通过HTTP请求来获取、创建、更新或删除数据(…

常见排序集锦-C语言实现数据结构

目录 排序的概念 常见排序集锦 1.直接插入排序 2.希尔排序 3.选择排序 4.堆排序 5.冒泡排序 6.快速排序 hoare 挖坑法 前后指针法 非递归 7.归并排序 非递归 排序实现接口 算法复杂度与稳定性分析 排序的概念 排序 &#xff1a;所谓排序&#xff0c;就是使一串记录&#…

Win11中zookeeper的下载与安装

下载步骤 打开浏览器&#xff0c;前往 Apache ZooKeeper 的官方网站&#xff1a;zookeeper官方。在主页上点击"Project"选项&#xff0c;并点击"Release" 点击Download按钮&#xff0c;跳转到下载目录 在下载页面中&#xff0c;选择版本号&#xff0c;并点…

什么是Pytorch?

当谈及深度学习框架时&#xff0c;PyTorch 是当今备受欢迎的选择之一。作为一个开源的机器学习库&#xff0c;PyTorch 为研究人员和开发者们提供了一个强大的工具来构建、训练以及部署各种深度学习模型。你可能会问&#xff0c;PyTorch 是什么&#xff0c;它有什么特点&#xf…

springboot、java实现调用企业微信接口向指定用户发送消息

因为项目的业务逻辑需要向指定用户发送企业微信消息&#xff0c;所以在这里记录一下 目录 引入相关依赖创建配置工具类创建发送消息类测试类最终效果 引入相关依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-…

【Python机器学习】实验14 手写体卷积神经网络(PyTorch实现)

文章目录 LeNet-5网络结构&#xff08;1&#xff09;卷积层C1&#xff08;2&#xff09;池化层S1&#xff08;3&#xff09;卷积层C2&#xff08;4&#xff09;池化层S2&#xff08;5&#xff09;卷积层C3&#xff08;6&#xff09;线性层F1&#xff08;7&#xff09;线性层F2 …

数据可视化-canvas-svg-Echarts

数据可视化 技术栈 canvas <canvas width"300" height"300"></canvas>当没有设置宽度和高度的时候&#xff0c;canvas 会初始化宽度为 300 像素和高度为 150 像素。切记不能通过样式去设置画布的宽度与高度宽高必须通过属性设置&#xff0c;…

Gateway网关路由以及predicates用法(项目中使用场景)

1.Gatewaynacos整合微服务 服务注册在nacos上&#xff0c;通过Gateway路由网关配置统一路由访问 这里主要通过yml方式说明&#xff1a; route: config: #type:database nacos yml data-type: yml group: DEFAULT_GROUP data-id: jeecg-gateway-router 配置路由&#xff1a;…

Liunx系统编程:进程信号的概念及产生方式

目录 一. 进程信号概述 1.1 生活中的信号 1.2 进程信号 1.3 信号的查看 二. 信号发送的本质 三. 信号产生的四种方式 3.1 按键产生信号 3.2 通过系统接口发送信号 3.2.1 kill -- 向指定进程发送信号 3.2.2 raise -- 当自身发送信号 3.2.3 abort -- 向自身发送进程终止…

使用 Elasticsearch 轻松进行中文文本分类

本文记录下使用 Elasticsearch 进行文本分类&#xff0c;当我第一次偶然发现 Elasticsearch 时&#xff0c;就被它的易用性、速度和配置选项所吸引。每次使用 Elasticsearch&#xff0c;我都能找到一种更为简单的方法来解决我一贯通过传统的自然语言处理 (NLP) 工具和技术来解决…

基于Python的HTTP代理爬虫开发初探

前言 随着互联网的发展&#xff0c;爬虫技术已经成为了信息采集、数据分析的重要手段。然而在进行爬虫开发的过程中&#xff0c;由于个人或机构的目的不同&#xff0c;也会面临一些访问限制或者防护措施。这时候&#xff0c;使用HTTP代理爬虫可以有效地解决这些问题&#xff0…

麦肯锡发布《2023科技趋势展望报告》,生成式AI、下一代软件开发成为趋势,软件测试如何贴合趋势?

近日&#xff0c;麦肯锡公司发布了《2023科技趋势展望报告》。报告列出了15个趋势&#xff0c;并把他们分为5大类&#xff0c;人工智能革命、构建数字未来、计算和连接的前沿、尖端工程技术和可持续发展。 类别一&#xff1a;人工智能革命 生成式AI 生成型人工智能标志着人工智…

元宇宙电商—NFG系统:区块链技术助力商品确权。

在国内&#xff0c;以“数字藏品”之名崛起以来&#xff0c;其与NFT的对比就从未停歇。从上链模式到数据主权&#xff0c;从炒作需求到实际应用&#xff0c;从售卖形式到价值属性&#xff0c;在各种抽丝剥茧般的比较中&#xff0c;围绕两者孰优孰劣的讨论不绝于耳。 NFT的每一…

机器学习知识点总结:什么是EM(最大期望值算法)

什么是EM(最大期望值算法) 在现实生活中&#xff0c;苹果百分百是苹果&#xff0c;梨百分白是梨。 生活中还有很多事物是概率分布&#xff0c;比如有多少人结了婚&#xff0c;又有多少人有工作&#xff0c; 如果我们想要调查人群中吸大麻者的比例呢&#xff1f;敏感问题很难得…

React如何配置env环境变量

React版本&#xff1a; "react": "^18.2.0" 1、在package.json平级目录下创建.env文件 2、在‘.env’文件里配置环境变量 【1】PUBLIC_URL 描述&#xff1a;编译时文件的base-href 官方描述&#xff1a; // We use PUBLIC_URL environment variable …
最新文章