C/C++(六)多态
本文将介绍C++的另一个基于继承的重要且复杂的机制,多态。
一、多态的概念
多态,就是多种形态,通俗来说就是不同的对象去完成某个行为,会产生不同的状态。
多态严格意义上分为静态多态与动态多态,我们平常说的多态一般指动态多态。(后文介绍的多态也是动态多态,只在本部分介绍一下静态多态)
1、静态多态
静态多态又称作静态绑定(早绑定、前期绑定),即在函数编译期间就决定了程序的行为(即函数名修饰规则,具体C/C++(二)中有详细描述)。
平常最经常用的静态多态就是函数重载。
2、动态多态
动态多态又称作后期绑定,在程序运行期间再根据具体拿到的类型来调用具体的函数,确认程序的具体行为。
我们平常说的多态一般指动态多态,静态动态一般就说函数重载。
![]()
重载(静态多态)、虚函数重写(动态多态)、隐藏的区别
二、多态(动态多态)
从技术方面来说,多态就是不同继承关系下的类对象,去调用同一函数(调用的函数必须是虚函数,后文会介绍),会产生不同行为。
1、多态的构成条件
1、调用的函数必须是虚函数,且派生类必须为基类的虚函数进行重写。
2、必须用父类的指针 / 引用来调用虚函数。
(为什么必须传父类的指针 / 引用?这里初步解释,后面会在原理部分详细解释——因为父子类的赋值兼容原则,子类可以切片赋值给父类,父类却不能赋值给子类,因为可能会缺成员)
(那又为什么必须传指针 / 引用?因为传对象的话,子类只会把父类的那一部分成员拷贝过去,但是不会拷贝虚函数表指针,就不能成功调用对应的虚函数了)
2、虚函数
被 virtual 修饰的类成员函数称为虚函数。
class Person { public:// 虚函数virtual void BuyTicket() { cout << "买票-全价" << endl;} };2.1 虚函数的重写(多态的条件之一)
如果派生类中存在与父类完全相同(函数名、函数返回值、函数参数都完全相同)的虚函数,就称作派生类的虚函数重写了父类的虚函数。
#include <iostream> using namespace std;class Person { public:virtual void BuyTicket(){cout << "全价购票" << endl;} };class Student :public Person { public:/*子类重写父类虚函数时,如果不加 virtual 关键字,虽然也可以构成重写(子类继承下来父类的虚函数,仍旧保持虚函数属性)但是这种写法不规范,可读性较差,建议不要这么做*/virtual void BuyTicket(){cout << "半价购票" << endl;} };void Test(Person& p) {p.BuyTicket(); }int main() {Person p;Student s;Test(p);Test(s);return 0; }![]()
运行结果可以发现,传父子类,分别调用父子类的虚函数 2.2 多态的两个特殊情况
2.1.1 协变(基类与派生类的虚函数返回值类型不同的时候)
当派生类重写父类虚函数的时候,基类与派生类的虚函数的返回值类型可以不同,但是必须是父类 / 子类的指针或引用。
当派生类虚函数返回值是父类 / 子类的指针或引用时,称作协变。
2.2.2 析构函数的重写
如果基类的析构函数也是虚函数,这个时候只要派生类定义了析构函数,不论是否加了 virtual 关键字,都视作对基类的析构函数构成重写。
(虽然基类和派生类的析构函数名字不同,看似违背了虚函数的重写原则,实际上编译器会对析构函数的名称做特殊处理,在编译后,所有析构函数的名称都会统一处理成 destructor)
#include <iostream> using namespace std;class Person { public:virtual ~Person() { cout << "~Person()" << endl; } };class Student : public Person { public:virtual~Student() { cout << "~Student()" << endl; } };// 只有派生类Student的析构函数也定义了析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。 int main() {Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0; }![]()
父类调用析构函数,子类调用析构函数,先调用里面的子类析构,再调用父类析构
3、C++11检测虚函数是否重写的两个关键字
从上文的介绍可以看出,C++对虚函数的重写要求比较严格。在有些情况下(比如函数名、返回值字母写反写错),可能会无法构成重写,导致无法构成多态。
但是这种错误在编译期间是不会报出的,只有在程序运行时才会发现,与预期结果不符,这个时候才来debug,得不偿失。
因此C++11标准提供了两个帮助用户检测是否完成重写的关键字:final 和 override
3.1 final
final 修饰某个虚函数,则这个虚函数不能再被重写
3.2 override
override 修饰派生类虚函数,检查派生类的虚函数是否基类的某个虚函数的重写,如果不是(比如拼写错了),编译报错。
4、纯虚函数与抽象类
在虚函数的后面加上 =0 ,这样的虚函数称作纯虚函数。
包含纯虚函数的类叫做抽象类(又叫接口类,在某类不代表具体实体的时候可以使用;另一个意义是说明多态想在其多个子类中实现),抽象类不能实例化出对象。
继承抽象类的派生类也不能实例化出对象,只有当这个派生类对纯虚函数进行重写,这个派生类才能实例化出对象。
因此纯虚函数在某种程度上间接强制了派生类的重写,更体现了接口继承思想。
(接口继承与实现继承:
普通函数的继承是一种实现继承,继承的是函数的实现,目的是使用这个函数;
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写达成多态。)
三、多态的实现原理(重点)
1、代码引入
#include <iostream>
using namespace std;class Test
{
public:virtual void test(){cout << _num << endl;}
private:int _num = 1;
};int main()
{Test t;printf("%d", sizeof(Test));
}
让我们猜猜,sizeof(Test) 应该是多少?
很多人可能会说,函数储存在代码段里,不算在类大小里面,那就应该是4字节(32位系统) / 8字节(64位系统)?
但实际上:
![]()
x86环境下 ![]()
x64环境下 这是为什么?
通过内存窗口的观察我们可以看见,Test对象里面除了储存了_num 成员变量,还储存了一个叫做_vfptr的指针变量,而一切指针变量大小在32位系统下都是4字节,在64位系统下都是8字节。
这个_vfptr是什么?这个指针我们叫做虚函数表指针,指向虚函数表。(v代表virtual,f 代表function)
2、虚函数表
虚函数表的本质,是储存着一个类里面的所有虚函数地址的一个指针数组。(一般情况下这个数组最后会放一个nullptr作为虚函数表的终止标记。)(注意:不是储存着虚函数,是储存着虚函数的地址,虚函数还是储存在代码段里的)
我们给出一个多态的代码:
#include <iostream> using namespace std;class Base { public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;} private:int _b = 1; };// 派生类Derive继承Base并重写Func1 class Derive : public Base { public:virtual void Func1(){cout << "Derive::Func1()" << endl;} private:int _d = 2; };int main() {Base b;Derive d;return 0; }调用一下监视窗口:
我们可以发现:
1、派生类对象 d 由两部分构成,继承自父类的成员,和自己的成员
2、派生类和父类都有一个虚函数表指针,指向各自的虚函数表,虚函数表里面储存着虚函数的地址。
3、派生类的虚函数表和父类的虚函数表不一样,由于Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1;派生类完成重写了的虚函数覆盖了原有的父类虚函数。
所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
4、派生类其实把父类的三个函数都继承了下来,但是由于Func3不是虚函数,所以并未放到虚函数表中。
派生类虚函数表的生成流程:
1、先把基类的虚函数表拷贝到自己的虚函数表中
2、如果派生类重写了某个虚函数,在虚函数表中用这个虚函数地址覆盖原父类的虚函数地址3、派生类如果自己增加了虚函数,按照在派生类中的声明次序依次放到派生类虚函数表的后3、多态的原理
3、多态的实现原理
还是直接上代码:
#include <iostream> using namespace std;class Person { public:virtual void BuyTicket() { cout << "买票-全价" << endl; } };class Student : public Person { public:virtual void BuyTicket() {cout << "买票-半价" << endl; } };void Func(Person& p) {p.BuyTicket(); }int main() {Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0; }![]()
多态实现概念图 ![]()
多态实现代码图 观察多态实现代码图:
观察红色箭头可以看到,p在指向mike对象时,p->BuyTicket从mike的虚表中找到的虚
函数是Person::BuyTicket。
观察蓝色箭头可以看到,p在指向johnson对象时,p->BuyTicket在johson的虚表中
找到的虚函数是Student::BuyTicket。
这样就实现出了不同类的对象去调用同一函数时,展现出不同的形态。
再看一下汇编代码:
// 与多态无关的汇编代码都已去除 void Func(Person* p) { ...p->BuyTicket(); // p中存的是mike对象的指针,将p移动到eax中 001940DE mov eax,dword ptr [p] // [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx 001940E1 mov edx,dword ptr [eax] // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax 00B823EE mov eax,dword ptr [edx] // call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来 以后到对象的中取找的。 001940EA call eax 00头1940EC cmp esi,esp } int main() { ... // 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调 用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址mike.BuyTicket(); 00195182 lea ecx,[mike] 00195185 call Person::BuyTicket (01914F6h) ... }就可以明白,满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的,因此叫做动态多态。
4、多态是如何实现的?(一句话总结)
首先,多态是一种基于继承和虚函数实现的机制,派生类必须实现对虚函数的重写,用来调用虚函数的函数必须传父类的指针或引用;然后,基类和派生类各有一张虚函数表,通过传参的不同(父类直接传,子类切片),对象内部的虚函数表指针会去各自的虚函数表里面寻找对应的虚函数地址,从而实现调用同名函数时产生不同的行为,达到多态的效果。
5、有关多态的一些小问题:
如果子类不重写虚函数,父子类的虚函数表一样吗?
储存的虚函数的地址是一样的,但是虚函数表毕竟是两张表,储存虚函数表的地方不一样,是分开存储的!
如果有许多同类对象,它们的虚函数表一样吗?
一样!同类对象共用一张虚函数表!
也就是说,虚函数表本质其实是个静态常量,被所有同类对象共享!
四、多继承关系下的虚函数表
之前所说的是单继承关系下的虚函数表,那么多继承关系下的虚函数表是什么样的?
(PS:菱形继承和菱形虚拟继承太过复杂,这里只介绍普通多继承)
继续上代码:
#include <iostream>
using namespace std;class Base1
{
public:virtual void func1(){cout << "Base1::func1" << endl;}virtual void func2(){cout << "Base1::func2" << endl;}
private:int b1;
};class Base2
{
public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;}
private:int b2;
};class Derive : public Base1, public Base2
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3" << endl;}
private:int d1;
};// 先给虚函数函数指针取个别名VFPTR
typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Derive d;/*思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr1、先取b的地址,强转成一个int*的指针2、再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针3、再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。4、虚表指针传递给PrintVTable进行打印虚表5、需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。*/VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

可以发现,派生类继承了几个包含虚函数的父类,就有几个虚函数表。(派生类自己独有的虚函数,会存放在第一个继承的基类的虚表里,但是由于编译器的BUG,并没有展示在内存窗口里面,可以通过下图观察到)
相关文章:
C/C++(六)多态
本文将介绍C的另一个基于继承的重要且复杂的机制,多态。 一、多态的概念 多态,就是多种形态,通俗来说就是不同的对象去完成某个行为,会产生不同的状态。 多态严格意义上分为静态多态与动态多态,我们平常说的多态一般…...
汽车及零配件企业海量文件数据如何管
汽车行业特点 汽车行业是工业企业皇冠上的一颗明珠,在国民经济中占据着举足轻重的地位。汽车行业具备技术密集、创新速度快、供应链复杂等特点,具体体现为: 技术密集:汽车行业是技术密集型行业,覆盖机械、电子、软件、…...
【AI学习】Mamba学习(十二):深入理解S4模型
#1024程序员节|征文# HiPPO的学习暂告一段落,按照“HiPPO->S4->Mamba 演化历程”,接着学习S4。 S4对应的论文:《Efficiently Modeling Long Sequences with Structured State Spaces》 文章链接:https://ar5iv…...
linux入门之必掌握知识点
#1024程序员节|征文# Linux基础 top命令详解 top命令是用来查看进程系统资源使用情况的工具,它可以动态的现实。 top命令执行后,按大写M可以按内存使用情况进行排序,大写P可以按CPU使用情况进行排序,大写H可以显示线…...
【Web.路由]——路由原理
这篇文章,我们来讲一讲什么是路由。 路由是 将用户请求地址映射为一个请求委托的过程,负责匹配传入的Http请求,然后将这些请求发送到应用的可执行终结点。 这里需要注意一个内容,发送到应用的可执行终结点。 路由的分类&#x…...
Spring Boot技术在中小企业设备管理中的应用
2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常…...
Lua表(Table)
软考鸭微信小程序 过软考,来软考鸭! 提供软考免费软考讲解视频、题库、软考试题、软考模考、软考查分、软考咨询等服务 Lua中的表(table)是一种核心数据结构,它既是数组也是字典,能够存储多种类型的数据,包括数字、字符…...
51单片机应用开发(进阶)---外部中断(按键+数码管显示0-F)
实现目标 1、巩固数码管、外部中断知识 2、具体实现:按键K4(INT1)每按一次,数码管从0依次递增显示至F,再按则循环显示。 一、共阳数码管 1.1 共阳数码管结构 1.2 共阳数码管码表 共阳不带小数点0-F段码为ÿ…...
怎么区分主谓宾I love you与主系表I am fine? 去掉宾语看句子完整性 主系表结构则侧重于描述主语的状态、特征或性质
主谓宾与主系表是英语句子结构中的两种基本类型,它们在关注点、动词分类以及句子完整性方面有所区别。具体分析如下: 关注点 主谓宾I love you:主谓宾结构主要关注动作和影响对象之间的关系[1]。这种结构强调的是动态和行为,通常描…...
私域流量运营的误区
私域流量运营是近年来营销领域的重要趋势,但在实际操作中,很多企业和个人容易陷入一些误区。以下是几个常见的私域流量运营误区及其解决方法: 1. 只关注流量,不重视内容 误区:许多运营者认为,只要吸引到足…...
VirtualBox虚拟机桥接模式固定ip详解
VirtualBox虚拟机桥接模式固定ip详解 VirtualBox 桥接设置Ubuntu 24.04使用固定IP问题记录 VirtualBox 桥接设置 为什么设置桥接模式?桥接模式可以实现物理机和虚拟机互相通信,虚拟机也可以访问互联网(推荐万金油),物…...
面试问题基础记录24/10/24
面试问题基础记录24/10/24 问题一:LoRA是用在节省资源的场景下,那么LoRA具体是节省了内存带宽还是显存呢?问题二:假如用pytorch完成一个分类任务,那么具体的流程是怎么样的?问题三:详细介绍一下…...
中国区 Microsoft365主页链接请您参考:
Microsoft365主页链接请您参考: Redirecting PPAC链接请您参考: Power Platform admin center 关于Power Automate开启工单是在 https://portal.partner.microsoftonline.cn/Support/SupportOverview.aspx进行提交的。 对应所需对应管理员可以分配以下…...
Go encoding/json库
JSON在网络上广泛使用,是一种基于文本的数据传输方式。在本集中,我们将与 Daniel Marti 一起探索 Go 的 encoding/json 包和其他包。 本篇内容是根据2020年7月份[#141 {“encoding”:“json”}](https://changelog.com/gotime/141 “#141 {“encoding”…...
「实战应用」如何用图表控件LightningChart可视化天气数据?(二)
LightningChart.NET完全由GPU加速,并且性能经过优化,可用于实时显示海量数据-超过10亿个数据点。 LightningChart包括广泛的2D,高级3D,Polar,Smith,3D饼/甜甜圈,地理地图和GIS图表以及适用于科学…...
苹果瑕疵数据集苹果质量数据集YOLO格式VOC格式 深度学习 目标检测 数据集
一、数据集概述 数据集名称:2类苹果图像数据集 数据集包含两类样本:正常苹果和有瑕疵的苹果。正常苹果样本代表完好的苹果,而有瑕疵的苹果样本代表苹果表面可能存在的损伤、瑕疵或病害。每个样本都经过详细标记和描述,以便训练模…...
旧电脑安装Win11提示“这台电脑当前不满足windows11系统要求”,安装中断。怎么办?
前言 最近有很多小伙伴也获取了LTSC版本的Win11镜像,很大一部分小伙伴安装这个系统也是比较顺利的。 有顺利安装完成的,肯定也有安装不顺利的。这都是很正常的事情,毕竟这个镜像对电脑硬件要求还是挺高的。 有一部分小伙伴在安装Windows11 …...
深入理解QT多线程编程
文章目录 多线程用法QThread类QtConcurrent类QFutureSynchronizer类获取线程信息线程优先级获取线程状态线程局部存储使用线程池监听线程事件Qt是一个跨平台的应用程序开发框架,广泛应用于图形用户界面(GUI)开发。它提供了强大的多线程支持,允许开发者在应用程序中创建和管理…...
React四官方文档总结一UI与交互
代码下载 React官网已经都是函数式组件文档,没有类组件文档,但是还是支持类组件这种写法。 UI 描述 组件 组件 是 React 的核心概念之一,它们是构建用户界面(UI)的基础。React 允许你将标签、CSS 和 JavaScript 组…...
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
文章目录 一、什么是 HTTP?无状态的含义 二、为什么 HTTP 是无状态的?三、Cookie 和 Session 的引入1. Cookie特点:示例: 2. Session特点:示例(Java Servlet): 四、HTTP、Cookie 和 …...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
c#开发AI模型对话
AI模型 前面已经介绍了一般AI模型本地部署,直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型,但是目前国内可能使用不多,至少实践例子很少看见。开发训练模型就不介绍了&am…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...
【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论
路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中(图1): mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...
离线语音识别方案分析
随着人工智能技术的不断发展,语音识别技术也得到了广泛的应用,从智能家居到车载系统,语音识别正在改变我们与设备的交互方式。尤其是离线语音识别,由于其在没有网络连接的情况下仍然能提供稳定、准确的语音处理能力,广…...





