【C++进阶】多态

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 一、 多态的概念
  • 二、多态的定义及实现
      • 2.1 虚函数
      • 2.2 虚函数的重写(覆盖)
      • 2.3 多态的构成条件(重点)
      • 2.4 多态构成条件的两个例外
      • 2.5 析构函数的重写(面试常考)
  • 三、override和final(支持C++11)
      • 3.1 override
      • 3.2 final
  • 四、重载、覆盖(重写)、隐藏(重定义)的对比
  • 五、多态原理
      • 5.1 虚函数表

一、 多态的概念

多态是面向对象三大基本特征中的最后一个。概念:通俗来说,就是多种形态,具体点就是当不同的对象去完成某个行为,就会产生出不同的状态。

比如在购买高铁票时,成人原价,学生半价,而军人可以优先购票,对于购票这一相同的动作,需要根据不同的对象提供不同的方法

#include <iostream>
using namespace std;class Adult // 成人
{
public:virtual void Buyticket(){cout << "成人-原价" << endl;}
};class Student : public Adult
{
public:virtual void Buyticket(){cout << "学生-半价" << endl;}
};class Soldier : public Adult
{
public:virtual void Buyticket(){cout << "军人-优先" << endl;}
};void Buyticket(Adult& At)
{At.Buyticket();
}int main()
{Adult at;Student s;Soldier sd;Buyticket(at); // 成人Buyticket(s); // 学生Buyticket(sd); // 军人return 0;
}

【输出结果】

可以看到,不同对象调用同一函数,执行结果是不同

在这里插入图片描述

二、多态的定义及实现

2.1 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

在这里插入图片描述

2.2 虚函数的重写(覆盖)

虚函数的重写(覆盖)子类中有一个跟父类完全相同的虚函数,即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同,称子类的虚函数重写了父类的虚函数。

// 父类
class Adult // 成人
{
public:virtual void Buyticket(){cout << "成人-原价" << endl;}
};// 子类
class Student : public Adult
{
public:virtual void Buyticket(){cout << "学生-半价" << endl;}
};

2.3 多态的构成条件(重点)

在继承中要构成多态还有两个条件

  1. 必须通过父类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写(返回值类型、函数名字、参数列表的类型完全相同)

注意:上述两个构成多态的条件缺一不可!缺少其中任意一个条件,都不构成多态!

// 父类
class Adult // 成人
{
public:virtual void Buyticket(){cout << "成人-原价" << endl;}
};// 子类
class Student : public Adult
{
public:// 子类必须对父类的虚函数进行重写virtual void Buyticket(){cout << "学生-半价" << endl;}
};// 1. 通过父类的指针或者引用调用虚函数
// void Buyticket(Adult* At) - 指针
void Buyticket(Adult& At) // 引用
{// 2. 被调用的函数必须是虚函数At.Buyticket();
}

2.4 多态构成条件的两个例外

  • 例外一:子类虚函数可以不使用virtual修饰

在这里插入图片描述

虽然这个例外在语法上是支持的,但是建议不要省略,因为会破坏代码的可阅读性,可能无法让别人一眼看出多态。

  • 例外二:协变(父类与子类虚函数返回值类型可以不同)

子类重写基类虚函数时,与父类虚函数返回值类型不同。即 父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。

第一种:返回各对象的指针

class Adult // 成人
{
public:// 父类虚函数返回父类对象的指针virtual Adult* Buyticket(){cout << "成人-原价" << endl;return 0;}
};
// 子类
class Student : public Adult
{
public:virtual Student* Buyticket(){cout << "学生-半价" << endl;return 0;}
};

第二种:返回各对象的引用

class Adult // 成人
{
public:// 父类虚函数返回父类对象的引用virtual const Adult& Buyticket(){cout << "成人-原价" << endl;return Adult(); // 返回匿名对象 }
};
// 子类
class Student : public Adult
{
public:virtual const Student& Buyticket(){cout << "学生-半价" << endl;return Student();}
};

注意:父子类关系的指针/引用,不是必须是自己的,也可以是其他类的,但是要对应匹配子类和父类。

class A  // 父类
{};class B : public A // 子类
{};class Adult // 成人
{
public:// 父类虚函数返回父类对象的引用virtual const A& Buyticket(){cout << "成人-原价" << endl;return A(); // 返回匿名对象 }
};
// 子类
class Student : public Adult
{
public:virtual const B& Buyticket(){cout << "学生-半价" << endl;return B();}
};

还有一点要注意的是,不可以一个是指针,一个是引用,必须同时是指针,或者同时是引用

2.5 析构函数的重写(面试常考)

有个问题:析构函数加上virtual是不是虚函数重写?

答案:是。虽然父类与子类析构函数名字不同(不满足三重),看起来违背了重写的规,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

#include <iostream>
using namespace std;class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A
{
public:virtual ~B(){cout << "~B()" << endl;}
};int  main()
{A a;B b;return 0;
}

【输出结果】

在这里插入图片描述

接下来就是面试官的连续“攻击”:为什么要这样处理呢?— 因为要构成重写

那么为什么要让它们构成重写呢?其实不加virtual关键字也是可以的

在这里插入图片描述

但如果不对析构重写的话,那么下面有一个场景是过不了的(记住此场景)

#include <iostream>
using namespace std;class A // 父类
{
public:~A(){cout << "~A()" << endl;}
};class B : public A // 子类
{
public:~B(){cout << "~B()" << endl;delete[] ptr;}
protected:int* ptr = new int[3];
};int  main()
{A* a = new A;delete a;a = new B;delete a;return 0;
}

【输出结果】

在这里插入图片描述

我们发现,不加virtual没有调用子类的析构函数,发生了内存泄漏。那为什么没有调到子类的析构呢?第一次释放了a指向的空间,然后又改变了指向

在前面说过,类的析构函数都被处理成了destructor这个函数。而delete对于自定义类型的原理是:

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间(operator delete本质就是调用free函数)

即对于delete a,先调用了析构函数a->destructor(),然后再调用operator delete函数释放对象的空间。

但由于编译器将析构函数名处理成一样的函数名destructor,因此构成了隐藏/重定义了。而且a刚好是A类型的指针,是一个普通的调用,不是多态调用。对于普通调用,看的是当前者的类型。因此delete a就会再次调用A类的析构函数。

但我们想的是指向什么类型,就去调用对应的析构函数,因此这是就得用到多态了。多态调用:看的是其指向的类型,指向什么类型,调用什么类型。

  • 因此,为什么要在 父类/基类 的析构函数中加上virtual修饰?

答案:为了构成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏。

三、override和final(支持C++11)

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写。

3.1 override

作用:修饰子类的虚函数,检查是否构成重写(是否满足重写的必要条件),若不满足,则报错

先以正确代码为例,override要写在子类函数括号的后面

#include <iostream>
using namespace std;class A
{
public:virtual void Print(){cout << "class A" << endl;}
};class B : public A
{
public:virtual void Print() override{cout << "class B : public A" << endl; }
};void Print(A& a)
{a.Print();
}int main()
{A a;B b;Print(a);Print(b);return 0;
}

【输出结果】

在这里插入图片描述

以下故意在子类的虚函数加个参数(不构成三重:子类虚函数与父类虚函数的返回值类型、函数名字、参数列表的类型完全相同),看看是否会报错

在这里插入图片描述

3.2 final

作用:修饰父类的虚函数,不让子类的虚函数与其构成重写,即不构成多态

对父类的虚函数加上final:无法构成重写

在这里插入图片描述

除此之外,final在某些场景下很实用:final还可以修饰父类,修饰后,父类不可被继承。

在这里插入图片描述

注:final可以修饰子类的虚函数,因为子类也有可能成为父类;但override无法修饰父类的虚函数,因为父类之上没有父类了,自然无法构成重写。

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

面试题中也喜欢考这三者的区别

  • 重载:即函数重载。在同一个作用域中,通过参数的类型、个数或顺序不同来定义多个具有相同函数名但不同参数列表的方法。重载方法在编译时根据调用的参数匹配最合适的方法。

  • 重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则(函数名、参数列表的类型和返回类型必须与父类中的方法一致)时,则会发生重写(覆盖)行为

  • 重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数。如果想使用父类的同名成员,可以通过::指定调用。

重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义。注:在类中,仅仅是函数名相同(未构成重写的情况下),就能触发 重定义(隐藏)

在这里插入图片描述

五、多态原理

5.1 虚函数表

多态究竟是如何实现的?先来看一段简单的代码,同时也是一道笔试题。

#include <iostream>
using namespace std;class A
{
public:virtual void Test(){cout << "class A" << endl;}
};int main()
{A a; cout << "A sizeof:" << sizeof(a) << endl;return 0;
}

【输出结果】

在这里插入图片描述

上述代码仅仅只是一个空类,只有一个虚函数,而我们知道对象是不存储函数的。而这里连成员变量也没有,结果为什么是4呢?当把环境改为64位平台(x64),输出结果却是8

在这里插入图片描述

这样下来,大小随平台而变的只能是指针了,因此可以推测当前类中藏着一个虚表指针

在这里插入图片描述

如上图所示,对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

另外,从这里我们也可以知道:一般不使用多态的话,最好还是不要加上virtual,因为是有开销的。


那么派生类中这个表放了些什么呢?我们接着往下分析

针对上面的代码我们做出以下改造

class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual void Func2(){cout << "A::Func2()" << endl;}void Func3(){cout << "A::Func3()" << endl;}protected:int _a = 1;
};class B : public A
{
public:virtual void Func1() override{cout << "B::Func1()" << endl;}
};int main()
{A a;B b;return 0;
}

在这里插入图片描述

vs的监视窗口中,可以看到只要涉及虚函数类的对象中都有属性__vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表。

  1. 子类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 父类a对象和子类b对象虚表是不一样的,这里我们发现Test完成了重写,所以b的虚表中存的是重写的B::Test,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func继承下来后是虚函数,所以放进了虚表,Print也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
  5. 总结一下子类的虚表生成:
    ①先将父类中的虚表内容拷贝一份到子类虚表中
    ②如果派生类重写了父类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数。
    ③子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后。

这里还有很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
答:很多人都认为虚函数存在虚表,虚表存在对象中。但是这是错的。
注意:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

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

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

相关文章

如何用Jmeter提取和引用Token

1.执行获取token接口 在结果树这里&#xff0c;使用$符号提取token值。 $根节点&#xff0c;$.data.token表示提取根节点下的data节点下的token节点的值。 2.使用json提取器&#xff0c;提取token 变量路径就是把在结果树提取的路径写上。 3.使用BeanShell取样器或者BeanShell后…

webpack打包常用配置项

webpack打包配置项 参考链接 文件结构&#xff1a;最基础版 先安装 npm i webpack webpack-cli --dev 运行命令&#xff1a;npx webpack 进行打包 1. 配置webpack.config.js文件&#xff1a; const path require(path); module.exports {mode: development, // 开发环境 …

MyBatis: 向oracle表中插入null字段的处理

一、可以在SQL中指定类型&#xff1a; Insert("insert into student values(#{name,jdbcTypeNULL},#{age})")int addStudent(Param("name")String name, Param("age") int age);二、可以进行全局配置&#xff08;单独使用MyBatis时可如下配置&am…

【MATLAB第72期】基于MATLAB的LightGbm(LGBM)梯度增强决策树多输入多输出回归预测模型

【MATLAB第72期】基于MATLAB的LightGbm(LGBM)梯度增强决策树多输入多输出回归预测模型 一、学习资料 (LGBM)是一种基于梯度增强决策树(GBDT)算法。 往期研究了多输入单输出回归预测方法&#xff0c;本次研究多输入多输出回归预测。 参考链接&#xff1a; lightgbm原理参考链…

【网络编程】C++实现网络通信服务器程序||计算机网络课设||Linux系统编程||TCP协议(附源码)

TCP网络服务器 &#x1f40d; 1.程序简洁&#x1f98e;2. 服务端ServerTcp程序介绍&#x1f996;3.线程池ThreadPool介绍&#x1f995; 4.任务类Task介绍&#x1f419;5. 客户端Client介绍&#x1f991;6.运行结果&#xff1a;&#x1f990; 7. 源码&#x1f99e;7.1 serverTcp…

亚马逊云科技人工智能内容审核服务:大大降低生成不安全内容的风险

生成式人工智能技术发展日新月异&#xff0c;现在已经能够根据文本输入生成文本和图像。Stable Diffusion是一种文本转图像模型&#xff0c;可以创建栩栩如生的图像应用。通过Amazon SageMaker JumpStart&#xff0c;使用Stable Diffusion模型轻松地从文本生成图像。 尽管生成式…

【文末送书】Matlab科学计算

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…

string

目录 六、STL简介 (一)什么是STL (二)STL的版本 (三)STL六大组件 七、string (一)标准库中的string 1、string类 2、string常用的接口 1)string类对象的常见构造 2)string类对象的容量操作 3)string类对象的访问及遍历操作 4)string类对象的修改操作 5)string类非成…

PMC在制造企业中发挥哪些价值?

导 读 ( 文/ 2127 ) PMC(生产计划与物料控制)是制造企业运行的核心&#xff0c;PMC的好坏可以直接影响客户的需求交付、生产进行的顺利与否、库存资金的占用、企业效率的提升等&#xff0c;在制造企业中起着关键的作用。它负责将生产计划转化为实际的生产活动&#xff0c;并确保…

python调用GPT实现:智能用例生成工具

工具作用&#xff1a; 根据输入的功能点&#xff0c;生成通用测试点 实现步骤 工具实现主要分2个步骤&#xff1a; 1.https请求调用Gpt,将返回响应结果保存为.md文件 2.用python实现 将 .md文件转换成.xmind文件 3.写个简单的前端页面&#xff0c;调用上述步骤接口 详细代…

计算机网络的故事——确保Web安全的Https

确保Web安全的Https 文章目录 确保Web安全的Https一、HTTP 的缺点二、HTTP 加密 认证 完整性保护 HTTPS 一、HTTP 的缺点 1、明文传输 通信加密&#xff0c;HTTP协议中没有加密机制&#xff0c;但是可以通过SSL(Secure Socket Layer&#xff0c;安全套接字层)或TLE(Transpor…

Qt应用开发(基础篇)——普通按钮类 QPushButton QCommandLinkButton

一、前言 QPushButton类继承于QAbstractButton&#xff0c;是一个命令按钮的小部件。 按钮基类 QAbstractButton 按钮或者命令按钮是所有图形界面框架最常见的部件&#xff0c;当按下按钮的时候触发命令、执行某些操作或者回答一个问题&#xff0c;典型的按钮有OK&#xff0c;A…

2023-09-09 LeetCode每日一题(课程表)

2023-09-09每日一题 一、题目编号 207. 课程表二、题目链接 点击跳转到题目位置 三、题目描述 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中…

初次安装Pytorch过程

第一次安装Pytorch&#xff0c;刚开始安装的时候装错了CUDA的版本号 这里最高支持12.2.138&#xff0c; 但是我装了一个12.2.140的CUDA&#xff0c;导致不兼容我在测试时发现 import torch# if torch.cuda.is_available(): # print("GPU可用") # else: # p…

Kotlin(五) 循环语句

目录 For循环 关键字 until step downTo Java中主要有两种循环语句&#xff1a;while循环和for循环。而Kotlin也提供了while循环和for循环&#xff0c;其中while循环不管是在语法还是使用技巧上都和Java中的while循环没有任何区别&#xff0c;因此我们就直接跳过不进行讲解…

记LGSVL本地编译记录

主要的编译参考来着官方文件 Unity安装 安装unity hub 安装2020.3.3f1在unity hub上 但是我发现没有2020.3.3f1&#xff0c;只有2020.3.3f1c1&#xff0c;其实c1就是中国版&#xff0c;没有什么影响 GIT安装 安装GIT安装Git LFS验证git-lfs(输出Git LFS initialized就&am…

嵌入式Linux驱动开发(LCD屏幕专题)(三)

1. 硬件相关的操作 LCD驱动程序的核心就是&#xff1a; 分配fb_info设置fb_info注册fb_info硬件相关的设置 硬件相关的设置又可以分为3部分&#xff1a; 引脚设置时钟设置LCD控制器设置 2. 在设备树里指定LCD参数 framebuffer-mylcd {compatible "100ask,lcd_drv&qu…

运维学习之部署Alertmanager-0.24.0

参考《监控系统部署prometheus基本功能》先完成prometheus部署。 参考《运维学习之采集器 node_exporter 1.3.1安装并使用》安装node_exporter。 下载 nohup wget https://github.com/prometheus/alertmanager/releases/download/v0.24.0/alertmanager-0.24.0.linux-amd64.ta…

oled或数码管点阵的字模矩阵的原理讲解

通过取模软件得到的T字符的矩阵分析 字模选项中常用的设置的意义&#xff1a; **字宽和字高&#xff1a;**显示字符能够使用的长宽灯数量&#xff0c;也可以理解为像素 **点阵格式&#xff1a;**需要考虑实际焊接电路。阴码&#xff1a;灯共阴极&#xff0c;控制器输出高电位&…

NFS文件共享系统(K8S)

概述 部署NFS文件共享服务&#xff0c;为Kubernetes提供NFS共享做准备 步骤 安装软件 yum -y install nfs-utils 配置NFS(exports) 编辑 /etc/exports 文件。每一行代表一个共享目录&#xff0c;描述目录如何共享 编写规则&#xff1a; # <共享目录> [客户端1 选项…