C++ - 多态语法 - 虚函数使用介绍

news2024/10/2 3:28:50

多态简单介绍

 多态就是多种形态,是不同的对象去完成同一个动作所产生的结果可能有多种。这种多种的形态我们称之为多态。

比如:我们在买票的时候的时候,可能有成人全价,儿童半价,军人免票等等。对于成人,儿童,军人这三个不同的对象,在买票同一动作当中,就产生了不同的结果。

多态的定义 和 实现

 多态出现在同一继承关系当中的不同类对象,比如上述说的 Person对象买票全价,Student对象买票半价。

多态的组成方面,有两大必须的条件

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
     

虚函数

在知道多态是如何构成之前,我们先来认识一种特殊的成员函数---虚函数

注意

  • 其中的 virtual 虽然可以用来修饰虚函数,和虚继承,但是此时的虚函数和虚继承没有任何关系,可以理解为 virtual 修饰函数就是虚函数;修饰继承关系及时虚继承。
  • 关于虚函数 virtual 的修饰,只要在 函数的返回值之前加上 vitual 修饰的函数就是虚函数了。
  • 只要类当中的成员函数可以加 virtual 修饰 变成虚函数,普通的全局函数是不能加 virtual 变成虚函数的

虚函数定义如:

class Person {
public:
    virtual void buy() { cout << "全价" << endl;}
};

全局函数不能加 virtual 修饰变成虚函数

 虚函数的重写

 虚函数和其他成员函数一样,但是虚函数有一个特征,虚函数支持重写(覆盖)

 如果在派生类当中,有一个和基类当中相同的虚函数(两者之间返回值,函数名,参数列表完全相同),我们认为,此时派生类重写了基类当中的虚函数

class Person
{
public:
	virtual void buy() {
		cout << "Perosn:全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void buy() {
		cout << "Student:半价" << endl;
	}
};

上述子类(student)就重写了 父类(Perosn)当中的 buy() 这个虚函数。

 对于上述 这种虚函数的使用场景(通过指针或者引用来调用虚函数)

void func(Person& people)
{
	people.buy();
}

int main()
{

	func(Person());

	func(Student());

	return 0;
}

输出:

Perosn:全价
Student:半价

这样的话,我们就可以做到类似于,自动识别对象,然后去购买不同的票了。

 请注意,我们在调用虚函数的时候,一定是使用 引用或者指针的方式来调用虚函数,而且子类父类当中的函数都应该是 virtual 修饰的,子类重写过的虚函数,否则无法实现多态(如下,我们把func()函数当中的 Person& 参数类型 改为 Person

void func(Person people)
{
	people.buy();
}

 输出:

Perosn:全价
Perosn:全价

我们发现结果都是 “全价”。没有多态现象出现。 

同样,如果父类的函数没有加 virtual 修饰,输出结果和上述一样,但是如果父类虚函数加了 virtual 修饰,子类函数没有加 virtual 修饰,是可以实现多态的。----但是就算能够实现多态,建议还是把子类和父类的虚函数都加上 virtual 修饰。

 编译器在这里支持,派生类不用加 virtual ,是因为,编译器对于派生类的检查只是检查,派生类符不符合 “三同”的 多态条件。不同,可能看该函数和父类当中的虚函数函数名相同,就是别成隐藏了;相同才会去认为该函数是虚函数的重写。

 因为 派生类 继承了 父类的 virtual 修饰的虚函数,而子类当中的 重写只是对 父类当中虚函数的实现部分进行 重写。

class Person
{
public:
	virtual void buy() {
		cout << "Perosn:全价" << endl;
	}
};

class Student : public Person
{
public:
	void buy() {
		cout << "Student:半价" << endl;
	}
};

void func(Person& people)
{
	people.buy();
}

int main()
{
	Person perosn;
	func(perosn);

	Student student;
	func(student);

	return 0;
}

 输出:

Perosn:全价
Student:半价

 像上述的实现多态例子中的 Student 类型 对象传参到 Person& 类型参数接收,这里发生了 子类 到 父类的 切割。

有了切割,当传入参数就是父类的时候,不需要切割,这类直接就是调用父类对象的引用来调用buy()这个函数;如果传入的是子类的话,就会发生切割,指向子类,此时就是子类的引用,所以调用的是子类的buy()函数。

 具体切割是如何切割法,可以看以下博客:C++ - 继承_chihiro1122的博客-CSDN博客

 但是这里就有一个问题,我们知道,对象当中只存储成员变量,不存储成员函数;而且就算是子类的引用,只是访问的是父类当中子类的那一部分成员,编译器在此处究竟是如何做到区分两个虚函数的呢
 

虚函数重写的两个特殊情况

 协变

 这种情况是 -- 基类和派生类虚函数的返回值类型不同

但是,这里虚函数的返回值类型是有规定的,如果是只是普通类型的返回值类型不同,是会报错的:

 如果不是协变引起的虚函数返回值类型不同,编译器是会报编译错误的。

 只允许 基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用的情况,而我们把这种称为 协变。(而且,父类虚函数 和 子类虚函数 的返回值类型 必须同时是 指针 或者 引用,如果是像 指针 和 引用 岔着用是不行的,编译器会报错

 如下代码所示:

class Person
{
public:
	virtual Person* buy() {
		cout << "Perosn:全价" << endl;
		return 0;
	}
};

class Student : public Person
{
public:
	Student* buy() {
		cout << "Student:半价" << endl;
		return 0;
	}
};

虽然协变指定是父类虚函数返回值是父类的指针或引用,紫烈虚函数返回值是子类的之怎或引用;但是,只要是满足继承关系的类,按照上述的方式去使用协变,也是可以的(就是说上述返回值不一定是 Person 和 Student,也可以是其他父子关系)。

 如下代码所示(在 Person 和 Student 的虚函数返回值类型使用 A 和  B 其他继承关系):


class A
{
public:

};

class B : public A
{
};

class Person
{
public:
	virtual A* buy() {
		cout << "Perosn:全价" << endl;
		return 0;
	}
};

class Student : public Person
{
public:
	B* buy() {
		cout << "Student:半价" << endl;
		return 0;
	}
};

但是协变是一个 坑,由上面说的种种细节可以看出来,细节很多,不好记。而且协变在日常当中的使用频率也很少。不如不支持这个语法。但是在学校考试 和 面试当中经常考。

 析构函数的重写

class Person {
public:
	virtual ~Person() {
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	virtual ~Student() { 
		cout << "~Student()" << endl; 
	}
};

虽然上述的 Person 和 Student 两个类的析构函数名字看上去不同,但是实际上,继承当中的 父类 和子类的 析构函数是可以 构成虚函数的

如上述例子, ~Perosn()和 ~Student()两个函数,子类可以重写。

之所以支持,是因为,类的虚构函数都被处理为了destructor 这个统一的名字。这样处理的目的也是为了让 子类和父类的析构函数构成重写。 

如果不这样处理,会出现一些问题,子类重写的话,会出现一些问题:
 

class Person {
public:
	 ~Person() {
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	 ~Student() { 
		cout << "~Student()" << endl; 
	}
};


int main()
{
	Person* p = new Person();
	delete p;

	p = new Student();
	delete p;

	return 0;
}

如上所示,我们希望输出的结果是 :

~Person()
~Student()
~Person()

但,实际输出却是:
 

~Person()
~Person()

出现这个问题的原因是也 p 指针的类型。我们知道,普通对象 看当前调用的类型来决定调用 哪一个对象的析构函数,当前调用者 (p) 的类型是 Person*,所以自然只会调用 Person 对象的析构函数,(对于 delete p ,释放顺序是 p->destructor  +  operator delete(p)  ),这里调用的是 Person的析构函数,但是这里我们不希望调用 Perosn的析构函数。

这里我们希望 p 指向那个对象就调用哪一个对象的析构函数,而不是看 p 指针的类型来决定调用哪一个对象的 析构函数。如果看类型的话,一直调用的就是 p 的类型的析构函数。但是 p 这个指针有可能指向父类,也有可能指向子类。

我们希望 p->destructor()调用的析构函数,是一个多态调用,而不是一个普通调用。        

 所以这里我们要使用多态来实现,在 detele 底层实现当中,就是使用 指针来调用 析构函数的,指针已经实现了,现在还差重写,所以才有了上述的 析构函数重写。

final   和  override

 上述我们也介绍了,如果实现函数重写,我们也发现,C++当中对于重写函数的规定还不少,缺一样都会导致重写失败。有些错误甚至在编译器时期是不会报错的,只有在程序运行之后才能发现问题,此时在发现问题就只能去debug,在代码量很多的场景当中,特别麻烦。

所以在C++11 当中新增了 两个关键词 final 和 override ,来帮助我们检查是否重写。

final:

final 关键字是用来阻止某一虚函数被子类重写:

 final 关键词修饰位置 和 之前 const 修饰 this 指针一样,是在 参数列表括号的右边。(而且只能放在父类的虚函数上)

 当父类的 虚函数被 final 修饰之后,子类就不能再重写父类的这个虚函数了。

 override:

override用于帮助派生类检查是否完成重写,如果没有,会报错:

 这样就方式我们因为,派生类没有重写完成,而导致后序debug的麻烦了。

  虚函数的指针 与 虚函数表 (多态的一些底层原理)

 下面这个例子,应该输出什么?

class Bass
{
public:
	virtual void func()
	{

	}

private:
	char _b;
};

int main()
{
	cout << sizeof(Bass) << endl;

	Bass b;

	return 0;
}

 上述输出不是1,而是8。我们知道,类的大小只计算成员大小,不计算函数。

我们打开调试发现,在 b 这个对象当中多了一个 _vfptr 指针(virtual function)。

 这指针是 虚函数表 指针,

这就是为什么,没有实现多态,不要把虚函数搞到类当中去;因为虚函数会被放进虚函数表当中;其实严格来说,虚函数还是存储在代码段当中的,而虚函数表当中存储的是各个虚函数的地址。

 这个虚函数表,在重写之后,会发生变化,我们来看下面这个例子:

class Person {
public:
	virtual void BuyTicket() {
		cout << "买票-全价" << endl;
	}
	int _a = 1;
};
class Student : public Person {
public:
	virtual void BuyTicket() {
		cout << "买票-半价" << endl;
	}
	int _b = 2;
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Student jason;

	Func(Mike);
	Func(jason);

	return 0;
}

 在上述这个代码当中,Mike 对象(父类对象)当中有下面这两个部分:

 _vfptr 是虚函数表的指针,此时的虚函数表当中存储的是 Person(父类)当中虚函数的地址,此时只有一个地址,因为只写了一个虚函数,如果有多个虚函数的话,有几个虚函数,虚函数表当中就有几个地址。

Jason对象(子类对象)当中有下面两个部分:

 我们发现,在子类对象 jason 当中有一个父类对象,父类对象当中也有一个虚函数表指针,此时虚函数表当中也只有一个地址,这个地址已经发生了改变,指向了子类重写的虚函数。

 总结:重写也可以叫覆盖,重写是我们写代码层面所看到的,覆盖是底层逻辑当中,子类重写的虚函数地址覆盖了父类虚函数的地址。

 此时我们就明白下面这个函数是如果实现,传入父类就调用父类的函数,传入子类就调用子类的函数了

  •  传入父类,看到就是父类,直接调用父类的函数;传入子类,切片之后看到的还是父类;
  • 如果是普通调用,在编译的时候就确定了地址,编译器判断是不是普通的调用很简单,看符不符合多态,不符合就是普通调用。
  • 如果是普通调用,就直接看p的类型,p的类型是Person,那么就直接在Person当中找到这个函数的地址,所以就不能实现多态。
  • 符合多态,就和上述说的一样,运行时到指向的对象的虚函数表当中,找调用。

重载,重写(覆盖),重定义(隐藏)的对比

 虚函数和多态的例题

 很多人,看到满足多态的条件,以为输出的是    B->0  ;但是实际输出却是  B->1

 我们发现,上述的func()函数,满足 虚函数重写,子类父类的虚函数函数名,返回值,参数类型和个数都是相同的(注意,不要看val 的缺省参数不同就认为这里不满足多态,参数列表相同只要求 参数个数 和 参数类型相同即可);

而且,在 test()函数当中调用的 func ()函数,使用指针调用的 ,因为 func()函数是本类当中的成员函数,本类当中的成员是需要用 this->func()  这样的形式来访问的;而这里的this指针是父类还是子类的 指针呢?

答案是父类的。因为,子类继承父类当中的成员,不是直接进行拷贝赋值,而是调用父类的构造函数,在子类当中构造出一个父类的子对象,这个子对象我们可以理解为子类当中父类对象成员。然而,test()函数是存在于代码段的,他也不是在子类和父类当中都有存在,也就是说,test()函数只在代码段当中存储了一份,而不是在子类和父类当中都存储了一份。

因为,父类对象是直接在子类当中存储的,子类不会单独的看test()函数,而是把父类对象看做是一个整体,test()就在这个整体当中,所以,test()对象当中的 调用 func()函数使用的this指针是 A*(父类指针)。

 而在主函数当中的指针p,指向的是 B (子类对象),又满足多态,所以此时肯定是调用子类当中的 func()函数,所以输出 B-> 是正确的。

但是,要注意的是,重写只是重写函数当中 实现部分,对于函数名,返回值,参数列表还是使用的是父类的。所以,此处的 val 的缺省参数才是 父类当中的1,而不是子类当中的0

 可以理解为,重写是,父类 的 函数名,返回值,参数列表   +   子类函数实现。

现在我们把上述例题修改一下,把 test()函数挪到 B 函数当中,其他不变:

 此时输出结果就是 B->0 了 。因为此时的 test()函数不满足多态的条件,此时的test()函数当中调用的 func()函数的 this 指针不是A*(父类指针)了,而是 B* (子类指针)

 所以,此时 test()当中的 调用 func()函数,就只是一个简单的 在本类当中调用本类的其他函数的情况。

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

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

相关文章

北斗高精度组合导航终端

UWB&#xff08;Ultra-Wideband&#xff09;、卫星定位&#xff08;GNSS&#xff09;&#xff0c;以及IMU&#xff08;Inertial Measurement Unit&#xff09;的组合定位系统结合了多种传感器和定位技术&#xff0c;以提供高精度、高可靠性的位置估计。这种组合定位系统在各种应…

五种定时任务方案(Timer+ScheduleExecutorService+spring task+多线程执行+quartz)

方案一&#xff1a;Timer (1)Timer.schedule(TimerTask task,Date time)安排在制定的时间执行指定的任务。 (2)Timer.schedule(TimerTask task,Date firstTime ,long period)安排指定的任务在指定的时间开始进行重复的固定延迟执行&#xff0e; (3)Timer.schedule(TimerTask…

网络技术十五:DHCP

DHCP 引入原因&#xff08;产生背景&#xff09; 手动为局域网中大量主机配置IP地址、掩码、网关等参数的工作繁琐&#xff0c;容易出错 DHCP可以自动为局域网中主机完成TCP/IP协议配置 DHCP自动配置避免了IP地址冲突的问题 定义 动态主机配置协议 用于为局域网中主机动态分…

文件怎么加密?文件加密软件哪个好用?

在电脑中使用重要文件时&#xff0c;我们需要保护文件的安全&#xff0c;避免数据泄露&#xff0c;而保护文件最好的方法就是文件加密。那么&#xff0c;文件该怎么加密呢&#xff1f; 超级加密3000 文件加密的首要任务就是保护数据内容的安全性&#xff0c;我们需要选择拥有超…

有哪些跨平台的办公软件,可以集成到自己的 APP中?

随着移动办公的普及&#xff0c;越来越多的跨平台办公软件被开发出来以满足不断增长的移动办公需求。这些软件不仅可以在各种操作系统中运行&#xff0c;还可以集成到自己的APP中&#xff0c;进一步提升用户的工作效率。本文将介绍一些常用的跨平台办公软件&#xff0c;并探讨如…

科普初步了解大模型

目录 一、大模型的简单认知 &#xff08;一&#xff09;官方定义 &#xff08;二&#xff09;聚焦到大语言模型 &#xff08;三&#xff09;大模型的应用举例 二、如何得到大模型 &#xff08;一&#xff09;整体的一般步骤 训练自己的模型 使用预训练模型 选择适当的…

2023年13个面向初学者最佳免费3D建模软件

推荐&#xff1a;使用 NSDT场景编辑器 快速搭建3D应用场景 现在有数百种不同的免费 3D 建模软件工具供希望创建自己的 3D 模型的用户使用——因此知道从哪里开始可能会很棘手。 3D 软件建模工具的范围从即使是最新的初学者也易于使用到可能需要数年才能学习的专业级软件——因…

一键部署k8s集群

前置动作 关闭防火墙 systemctl disable firewalld && systemctl stop firewalld 关闭SELinux sed -i s#SELINUXenforcing#SELINUXdisabled#g /etc/selinux/config && grep SELINUXdisabled /etc/selinux/config setenforce 0 getenforce 关闭swap # 关闭…

Linux Kafka 3.5 KRaft模式集群部署

这里是weihubeats,觉得文章不错可以关注公众号小奏技术&#xff0c;文章首发。拒绝营销号&#xff0c;拒绝标题党 背景 kafka在KIP-500引入了KRaft替代Zookeeper来实现自我管理元数据 详细信息可以看原文链接 KIP-500 KRaft简介 KRaft是kafka用来取代zookeeper的分布式协调…

号外号外,桓峰基因单细胞生信分析免费培训课程即将开始,快来报名吧!

单细胞生信分析教程 桓峰基因公众号推出单细胞生信分析教程并配有视频在线教程&#xff0c;目前整理出来的相关教程目录如下&#xff1a; Topic 6. 克隆进化之 Canopy Topic 7. 克隆进化之 Cardelino Topic 8. 克隆进化之 RobustClone SCS【1】今天开启单细胞之旅&#xff0c;述…

FPGA实战小项目3

基于FPGA的波形发生器 基于FPGA的波形发生器 基于FPGA的beep音乐播放器设计 基于FPGA的beep音乐播放器设计 基于FPGA的cordic算法实现DDS sin和cosine波形的产生 基于FPGA的cordic算法实现DDS sin和cosine波形的产生

PY32F003F18端口复用功能映射

PY32F003F18端口复用功能映射&#xff0c;GPIO引脚可配置为"输入&#xff0c;输出,模拟或复用功能。 一、端口A复用功能映射 端口A复用功能映射表里&#xff0c;每个引脚都有AF0~AF15&#xff0c;修改AF0~AF15的值&#xff0c;就可以将对应复用用能引脚映射到CPU引脚上。…

【pthreads】支持vs2022构建

新增了一个vc16的目录 https://github.com/BrianGladman/pthreads/commit/60179353ef753ca171dee5199ec5fa54580835b0 官方已经支持直接支持v143 下载最新版,直接打开build.vs目录的sln vs 会提示安装python调试环境 安装过程自己失败取消了 再次打开就没弹出了。 直接构建静态…

2023全国大学生数学建模A题B题C题D题E题竞赛选题建议,思路模型

目录 国赛数学建模思路模型代码&#xff1a;9.7开赛后第一时间更新&#xff0c;完整思路获取见文末名片 一、题目选择 二、国赛摘要及论文写作技巧 1、国赛摘要 2、论文写作技巧 三、历年国赛真题及对应算法模型 完整国赛题思路模型获取见此 国赛数学建模思路模型代码&am…

企业架构LNMP学习笔记20

Nginx Location匹配规则&#xff1a; URI&#xff1a;统一资源标识符。 URN&#xff1a;统一资源名称。 URL&#xff1a;统一资源定位符。URL是更细化一点。 1&#xff09;精确匹配&#xff1a; location / {#规则 } 则匹配到 Example Domain 这种请求。 2&#xff09;~ 大…

RocketMQ consumer 和 queue 对应关系

参考 Consumer and Consumer Group Load Balancing https://rocketmq.apache.org/docs/4.x/consumer/01concept2 旧版本MQ结论 消费者应用和topic队列一对多的关系。 &#xff08;一个消费组consumer group里&#xff0c;一个消费者应用可以消费多个队列的消息。一个队列的消…

OpenMLDB 基于 Kubernetes 的部署全攻略

简介 Kubernetes 作为当前工业界流行的云原生容器编排和管理工具&#xff0c;在大量项目实践中被使用。目前&#xff0c;OpenMLDB 的离线引擎和在线引擎&#xff0c;均已经完整支持了基于 Kubernetes 的部署&#xff0c;可以实现更为方便的管理功能。本文将分别介绍离线和在线…

超级好用的10个思维导图模板

思维导图是一种非常有用的工具&#xff0c;可以被广泛应用于不同领域的人群。学生可以用思维导图来整理知识&#xff0c;老师可以用思维导图规划教学内容&#xff0c;设计课堂活动&#xff0c;还可以帮助学生梳理知识结构。各行各业的人都可以运用思维导图处理自己工作中的问题…

openWRT SFTP 远程文件传输

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

如何阻止事件冒泡(event bubbling)?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 原生 JavaScript⭐ jQuery⭐ React⭐Vue.js⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对…