【C++】“多态”特性

article/2025/8/22 23:49:14

在这里插入图片描述

文章目录

  • 一、多态的概念
  • 二、多态的定义实现
    • 1. 多态的构成条件
      • 1.1 虚函数
      • 1.2 虚函数的重写
    • 2. 多态的调用
    • 3. 虚函数重写的其他问题
      • 3.1 协变
      • 3.2 析构函数的重写
  • 三、override和final关键字
  • 四、重载/重写/隐藏的对比
  • 五、纯虚函数和抽象类
  • 六、多态的原理

C++的三大主要特性:封装(类和对象)、继承、多态。前两者我们已经学习过了,今天最后来认识一下“多态”特性。

一、多态的概念

多态,就是“多种形态”,指函数的行为可以有多种形态。多态一般分为编译时多态(静态多态)和运行时多态(动态多态)。其中,编译时多态主要就是之前讲的函数重载和函数模板,它们传不同类型的参数就可以调用不同的函数,通过参数的不同达到不同的形态效果,之所以叫编译时多态,是因为传参匹配这一过程是在编译时期完成的。
运行时多态,具体指一个函数传不同的对象就会完成不同的行为,达到多种形态。比如写一个买火车票行为,传普通人类对象是“全价”,传学生类对象是“打折”,传军人类对象就是“优先”,一个函数(行为),根据传的对象类型不同,就有不同作用。今天谈的“多态”指的主要就是运行时多态。

二、多态的定义实现

1. 多态的构成条件

(运行时)多态是一个继承体系下的类对象,去调用同一函数,而产生不同的行为。比如,Person类为基类,Student类继承了Person类,Soldier类继承了Person类,那么这三类的对象买票行为就有不同的效果。

实现多态还有两个重要的条件:

  • 必须是基类的指针类型或引用类型去调用虚函数。
  • 被调用的函数必须是虚函数,并且完成了虚函数的重写(覆盖)。

我们依次来说明:
要实现多态效果,首先必须是基类的指针或引用去调用,因为只有这样才既能指向基类对象又能指向派生类对象(的基类的切片)。
第二,派生类必须对基类的虚函数完成重写,重写了,基类和派生类才能有这个函数的不同实现方法,多态的不同形态效果才能达到。

1.1 虚函数

类的成员函数前加关键字virtual修饰,那么这个成员函数被称为虚函数,非成员函数不能加virtual修饰。如:

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

虚函数的存在就是为了给多态服务的。

1.2 虚函数的重写

虚函数的重写指:派生类中有一个跟基类虚函数“三同”的虚函数,则称派生类的这个虚函数完成了对基类虚函数的重写。“三同”指的是,函数名相同、返回类型相同、参数类型。
在派生类中重写基类虚函数时,派生类的虚函数前可以不加virtual修饰,也可以构成重写,因为继承后基类的虚函数在派生类中仍保持虚函数属性。但是这种写法不规范,实际使用还是建议派生类的虚函数前写上virtual(但基类的虚函数前是必须写virtual的)。不过在考试中也有可能故意埋这个坑,注意判断。
举例:

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}
};class Student : public Person
{
public:// 注意保证“三同”virtual void BuyTicket(){cout << "打折" << endl;}
};

2. 多态的调用

利用多态的特性,我们可以模拟出简单的买火车票行为:传普通人类对象是“全价”,传学生类对象是“打折”,传军人类对象就是“优先”:

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "打折" << endl;}
};class Soldier : public Person
{
public:virtual void BuyTicket(){cout << "优先" << endl;}
};

但是记得刚才说的多态的第一个条件,必须是基类的指针类型或引用类型去调用虚函数,比如:
在这里插入图片描述

3. 虚函数重写的其他问题

3.1 协变

派生类重写基类虚函数时,重写时虚函数返回类型也可以不一样。基类虚函数可以返回自己或其他基类对象的指针或引用,派生类虚函数可以返回自己或其他派生类对象的指针或引用。这种方式称为卸变,但是它的实际意义并不大,简单了解即可。

class Person
{
public:virtual Person* BuyTicket(){cout << "全价" << endl;return nullptr;}
};class Student : public Person
{
public:virtual Student* BuyTicket(){cout << "打折" << endl;return nullptr;}
};

class A
{ };class B : public A
{ };class Person
{
public:virtual A* BuyTicket(){cout << "全价" << endl;return nullptr;}
};class Student : public Person
{
public:virtual B* BuyTicket(){cout << "打折" << endl;return nullptr;}
};

3.2 析构函数的重写

析构函数十分特殊,编译器会对析构函数的名字进行特殊处理——编译后析构函数的名称会统一处理成destructor,再加上析构函数都是没有返回、没有参数的。因此,基类和派生类的析构函数总会构成“三同”,如果不加virtual修饰成虚函数,那么基类和析构函数既然是“同名”的,就构成隐藏关系了所以,基类的析构函数一定要加virtual修饰成虚函数,派生类析构函数的virtual可写可不写

比如:

class A
{
public:~A(){cout << "~A()" << endl;}
};class B : public A
{
public:~B(){cout << "~B()" << endl;delete p;}
protected:int* p = new int[10];
};

在这里插入图片描述
可见,如果~A()不加virtual,那么delete p2时只会调用A的析构函数,没有调用B的析构函数,导致了内存泄漏问题。

这样就没问题了。
在这里插入图片描述

总而言之,基类的析构函数建议设计为虚函数

三、override和final关键字

C++提供了两个新的关键字:

  • override:
    C++对虚函数重写的要求是很严格的,但是有时候我们可能粗心没有满足多态的要求,但是语法没问题编译也不会有问题,只有在运行结果不是预期情况下我们才能发现问题。因此C++11提供了override关键字,写在派生类的想要重写的虚函数后,帮助用户检测是否完成了重写
    在这里插入图片描述
  • final
    之前继承提到过,如果一个类不想被继承,就在类名后加final修饰。除此之外final还有一个功能,如果我们不想让基类的虚函数被重写,就在这个虚函数()后加final修饰在这里插入图片描述

四、重载/重写/隐藏的对比

这三个概念比较相近,要注意区别:

函数重载:

  • 两个函数在同一作用域
  • 函数名相同,参数的类型或个数不同,返回值可同可不同

虚函数重写:

  • 两个函数分别在一个继承体系的基类和派生类中
  • 函数名、参数、返回值相同,协变除外
  • 两个函数必须都是虚函数

隐藏:

  • 两个函数分别在一个继承体系的基类和派生类中
  • 函数名相同
  • 两个函数只要不构成重写,就是隐藏关系
  • 基类和派生类的成员变量相同也构成隐藏关系

五、纯虚函数和抽象类

如果在一个虚函数的()后写上= 0,则这个虚函数称为纯虚函数,纯虚函数不需要定义实现(可以实现但是没有意义),只要声明即可。
包含纯虚函数的类称为抽象类,抽象类不能实例化出对象,如果一个抽象类的派生类继承后不重写纯虚函数,则派生类也是抽象类。可以认为,纯虚函数一定程度上强制派生类重写虚函数,因为不重写不实例化出对象。

在这里插入图片描述

六、多态的原理

还是看一开始的例子:

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "打折" << endl;}
};class Soldier : public Person
{
public:virtual void BuyTicket(){cout << "优先" << endl;}
};
int main()
{Person per;Student stu;Soldier sol;Person* ptr;ptr = &per;ptr->BuyTicket(); ptr = &stu;ptr->BuyTicket();ptr = &sol;ptr->BuyTicket();return 0;
}

调试观察,可以发现:
在这里插入图片描述
每一个对象的第一个成员都是一个叫__vfptr的指针(有些平台可能会把它放在最后一个),这个指针叫做虚函数表指针,指向这个类的虚函数表。一个含有虚函数的类中都至少有一个虚函数表指针,一个类所有虚函数的地址都会存在一个虚函数表中,虚函数表实际上就是函数指针数组,也称虚表。

  • 基类和每个派生类都有自己独立的虚函数表。派生类会继承基类的虚函数表指针,但是这个虚函数表指针和基类的虚函数表指针不是同一个指针,指向的虚表也就不是同一个。
  • 派生类重写了基类的虚函数后,派生类的虚表中对应的原基类虚函数就会被覆盖成派生类重写的虚函数地址。这也是为什么重写也可以称为覆盖。

基于这样的原理,基类的指针或引用调用基类和派生类的同一个虚函数时,其实就是从它们各自的__vfptr找到各自的虚函数表,再找到这个虚函数。但由于派生类的虚函数表中这个虚函数已经被重写(覆盖),调用后也就有不同的结果了。这就是多态实现的原理。

本篇完,感谢阅读。


http://www.hkcw.cn/article/CMeBbKVvDB.shtml

相关文章

SmolDocling-256M:极小参数量的视觉语言模型|端到端文档解析方案的另一种思路

背景问题 传统的一站式文档解析工具&#xff0c;包含布局分析、OCR和表格识别等&#xff0c;往往需要结合多个独立的模型&#xff0c;同时根据处理任务的不同调用不同的模型&#xff0c;增加了处理流程的复杂度&#xff0c;并且难以泛化到不同的文档类型。大型视觉语言模型&am…

SUV行驶中被巨石砸下路面,目击者:SUV司机自己爬上来,没受伤!

SUV行驶中被巨石砸下路面。5月28日贵州毕节,SUV行驶中被巨石砸下路面,摩托车司机弃车避险后又赶来查看,目击者:SUV司机自己爬上来,没受伤!SUV行驶中被巨石砸下路面SUV行驶中被巨石砸下路面SUV行驶中被巨石砸下路面SUV行驶中被巨石砸下路面SUV行驶中被巨石砸下路面责任编辑…

一文了解半导体封装测试

1.半导体后端工艺 制作半导体产品的第一步&#xff0c;就是根据所需功能设计芯片&#xff08;Chip&#xff09;。然后&#xff0c;再将芯片制作成晶圆&#xff08;Wafer&#xff09;。由于晶圆由芯片反复排列而成&#xff0c;当我们细看已完成的晶圆时&#xff0c;可以看到上面…

leetcode hot100刷题日记——28.环形链表2

解答&#xff1a; 方法一&#xff1a;哈希表 class Solution { public:ListNode *detectCycle(ListNode *head) {//哈希表unordered_set<ListNode *>visited;while(head!nullptr){if(visited.count(head)){return head;}visited.insert(head);headhead->next;}return…

NW907NW918美光固态闪存NW920NW930

NW907NW918美光固态闪存NW920NW930 技术解析&#xff1a;美光NW系列固态闪存的核心突破 美光NW907、NW918、NW920、NW930四款固态闪存产品&#xff0c;代表了当前存储技术的顶尖水平。其核心创新在于G9 NAND架构的深度优化&#xff0c;采用更先进的5纳米制程工艺&#xff0c;…

前人栽树,后人乘凉——AdaBoost

一、AdaBoost介绍 AdaBoost的全称是ADAPTIVE BOOSTING&#xff08;自适应增强算法&#xff09;&#xff0c;是一种经典的集成学习算法&#xff0c;它通过组合多个弱学习器来构建一个强学习器。 从表意上看&#xff0c;AdaBoost就是在不断对于错误的知识点进行加深印象&#x…

【深度学习:进阶篇】--2.3.深度学习正则化

学习目标 目标 了解偏差与方差的意义知道L2正则化与L1正则化的数学意义知道Droupout正则化的方法了解早停止法、数据增强法的其它正则化方式 应用 无 目录 学习目标 1 偏差与方差 1.1 数据集划分 1.2 偏差与方差 1.3 解决方法&#xff08;过拟合&#xff09; 2 正则化(…

解决报错error: ‘void_t’ is not a member of ‘std’

解决报错error: ‘void_t’ is not a member of ‘std’ 博主是在编译ceres库时遇到的此报错。 解决方式很简单&#xff0c;将编译使用的c标准设定为c17即可。 例如&#xff0c;在VS2022中&#xff0c;右键单击项目-属性&#xff1a;

【达梦数据库】会话sp_close关闭不掉

背景 一个纯查询的语句&#xff0c;执行了很久&#xff0c;sp_close关闭不掉 排查方法 1、会话sp_close关闭不掉&#xff0c;sp_cance后再执行sp_close依旧关闭不了&#xff1b; sp_close_session(sess_id)sp_cancel_session_operation(sess_id)2、通过分析事务视图v$trx的…

澳门向永久居民每人发1万澳门元 新计划细节公布

澳门特区行政会今日完成讨论《2025年度现金分享计划》行政法规草案。该法规对2025年度的现金分享发放资格及申请手续进行了规范。根据规定,符合身份条件和在澳条件的居民可获得现金分享,其中永久性居民每人一万澳门元,非永久性居民每人六千澳门元。身份条件是指在2024年12月…

接口测试之文件上传(全)

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 在日常工作中&#xff0c;经常有上传文件功能的测试场景&#xff0c;因此&#xff0c;本文介绍两种主流编写上传文件接口测试脚本的方法。 首先&#xff0c;要知道…

数十家超市曝出食品安全问题 供应链隐患凸显

近日,全国多地超市接连曝出食品安全问题,涉及蔬菜、肉类、包装食品等多个品类。永辉、麦德龙、山姆会员店、小象超市、盒马、中百、朴朴、沃尔玛、奥乐齐和大润发等多家超市被点名。在12315、黑猫和消费保等投诉平台上,可以看到山姆永辉等超市因食品安全问题遭到消费者投诉。…

canvas 实现全屏倾斜重复水印

​ 参考&#xff1a; html、js、canvas实现水印_html页面使用canvas绘制重复水印-CSDN博客 效果 ​​​​ 不求水印显示完全。 实现代码 <template><div class"watermark" ref"waterMark"></div></template><script lang&q…

Android Studio 2022.2.1.20 汉化教程

查看Android Studio 版本 Android Studio Flamingo | 2022.2.1 Patch 2 下载&#xff1a;https://plugins.jetbrains.com/plugin/13710-chinese-simplified-language-pack----/versions/stable

一根开价10万 年轻人迷上文玩玉米,风靡年轻圈

一根开价10万年轻人迷上文玩玉米。“文玩玉米风靡年轻圈,单根标价10万仍抢手!特殊培育的琥珀纹路、蜜蜡颗粒,经直播带货引爆市场,产业链从云南育种到百倍溢价,争议中见证新消费魔力。”文玩,这个曾被贴上“中老年专属”标签的传统爱好,如今正以意想不到的方式在年轻人中…

如何在Qt中绘制一个带有动画的弧形进度条?

如何在Qt中绘制一个弧形的进度条 在图形用户界面开发中&#xff0c;进度指示控件&#xff08;Progress Widget&#xff09;是非常常见且实用的组件。CCArcProgressWidget 是一个继承自 QWidget 的自定义控件&#xff0c;用于绘制圆弧形进度条。当然&#xff0c;笔者看了眼公开…

体育平台数据服务解决方案:从痛点到落地的全栈技术支持

一、体育平台开发的数据痛点解析 在体育平台开发领域&#xff0c;数据层建设往往成为技术团队的核心挑战&#xff1a; 实时数据获取难自建实时数据采集系统需解决赛事方反爬机制、多源数据同步&#xff08;如比分 / 球员位置 / 赔率&#xff09;、毫秒级延迟控制等问题&#…

去寺庙减脂的人胖了11斤 素食诱惑难抵挡

去寺庙减脂的人胖了11斤!一群原本希望通过吃减脂餐修身养性的打工人和中年人,在体验后第一批人竟胖了11斤,这反转实在惊人。当下生活节奏飞快,很多人被亚健康困扰,身体各种不适。于是大家开始注重养生,寺庙因宁静祥和且素食养生概念深入人心,被不少人视为减脂圣地。人们…

printf 输出格式总结(C语言)和C 中的类型提升规则、默认整型提升

类型格式符示例输出说明十进制整数%d12345输出有符号十进制&#xff08;int 类型&#xff09;十进制整数%u12345输出无符号十进制&#xff08;unsigned int&#xff09;十六进制%x3fa2小写十六进制&#xff08;无前缀&#xff09;十六进制%X3FA2大写十六进制&#xff08;无前缀…

Flutter下的一点实践

目录 1、背景2、refena创世纪代码3、localsend里refena的刷新3.1 初始状态3.2 发起设备扫描流程3.3 扫描过程3.3 刷新界面 4.localsend的设备扫描流程4.1 UDP广播设备注册流程4.2 TCP/HTTP设备注册流程4.3 localsend的服务器初始化工作4.4总结 1、背景 在很久以前&#xff0c;…