C++ 多态以及多态的原理
文章目录
- 多态的概念
- 多态的构成条件
- 虚函数的重写
- 虚函数重写的两个例外
- 重载、重写(覆盖)、重定义(隐藏)对比
- C++11 final 和 override关键字
- 抽象类
- 接口继承和普通继承
- 多态的原理
- 虚函数表
- 多态的原理
- 单继承和多继承关系的虚函数表
- 单继承中的虚函数表
- 多继承中的虚函数表
多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态
比如,买票时都是同一个景点有学生票半价和成人票全价等等
多态的构成条件
多态的构成条件主要涉及两个概念:虚函数和继承。
虚函数
- 虚函数是C++中用于实现运行时多态性的关键概念。
- 被virtual修饰的类成员函数称为虚函数
class Person {
public://虚函数virtual void BuyTicket() {//....}
};
在继承中构成多态还需要满足两个条件
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 父类的指针或者引用进行调用
举个栗子
class Person {
public://虚函数virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
class Student : public Person
{//重写基类函数virtual void BuyTicket(){cout << "买票-半价" << endl;}
};
//必须是基类对象指针或引用调用
void Func(Person& people)
{people.BuyTicket();
}
void Test()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);
}
int main()
{Test();return 0;
}
父类对象和子类对象调用同一个函数,得到的结果不一样
运行结果:

解释:

虚函数的重写
虚函数重写(也叫覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
在上面的例子中 派生类Student中的BuyTicket函数就重写了基类Person的虚函数
虚函数重写的两个例外
- 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同,且返回值必须是父子关系的指针或者引用。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
比如
class Person
{
public:// 基类虚函数返回基类指针virtual Person* BuyTicket() {return new Person();}
};
class Student : public Person
{// 派生类协变,返回更具体的类型 Student*virtual Student* BuyTicket(){return new Student();}
};
上面例子中也构成虚函数的重写,派生类和基类的返回值不同,称为协变
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;}
};
class Student : public Person
{
public:~Student(){cout << "~Student()" << endl;}
};
int main()
{Person* p1 = new Person();Person* p2 = new Student();// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数//才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。delete p1;delete p2;
}
运行结果:

12行加不加virtual关键字 都构成重写
重载、重写(覆盖)、重定义(隐藏)对比

C++11 final 和 override关键字
C++对函数重写的要求比较严格,有些情况可能由于疏忽,导致无法构成重写,这种情况编译器不会报错,程序会正常运行,但是得到的结果不是正确的,所以C++11引入了final和override关键字
- final 修饰虚函数,表示该虚函数不能再被重写
class Person
{virtual void Func() final{cout << "virtual void Func() final" << endl;}
};class Student : public Person
{virtual void Func()//error{cout << "virtual void Func()" << endl;}
};
编译时报错:

- override 检查派生类是否重写了基类虚函数。如果没重写,编译器会报错。
class Person
{
public:virtual void Func() const {cout << "virtual void Func() " << endl;}
};class Student : public Person
{
public:virtual void Func() override//error 派生类没有正确重写基类Func函数 编译器会报错,少了const修饰{cout << "virtual void Func()" << endl;}
};
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Person
{
public:virtual void Abstract() = 0;//纯虚函数 Person类为抽象类 Person类不能实例化出对象
};//派生类
class Student : public Person
{
public:virtual void Abstract() override//派生类必须重写纯虚函数,派生类才可以实例化出对象,否则不行{cout << "Hello World\n";}};
int main()
{//Person p1; //error 抽象类无法实例化对象Student s1;Person* s1prt = &s1;//使用基类指针访问s1prt->Abstract();return 0;
}
接口继承和普通继承
- 普通继续 派生类继承了基类,可以使用函数,继承的是函数的实现
- 接口继承,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
虚函数表
#include <iostream>
using namespace std;
class Person
{
public:virtual void Func();
private:int _a;char _b;
};
int main()
{cout << sizeof(Person) << endl;return 0;
}
上面代码求Person所占字节数大小
按照内存对齐的规则,Person类的大小应该是8(32位下)。
但实际结果是12

这里不仅要内存对齐,当实例化一个对象后发现,成员变量不仅仅只有_a,和_b 。还有一个指针_vfptr(虚函数表指针)

- 一个含有虚函数的类都会有至少一个虚函数表指针,虚函数表指针存放在对象的前4个字节或者前8个字节(32位下4个字节,64位下8个字节)。
- 而虚函数的地址会被存放到虚函数表中。虚函数表也简称为虚表
- 虚函数表指针指向虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(VS编译器下做了处理,g++没有处理)

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
通过下面的代码进行分析派生类中的虚表。
class Person
{
public:virtual void Func(){cout << "virtual void Func()" << endl;}virtual void Func2(){cout << "virtual void Func()" << endl;}void Func3()//普通函数{cout << "void Func3()" << endl;}
private:int _a = 0;char _b = 0;
};
class Student : public Person
{virtual void Func() override//重写基类函数{cout << "virtual void Func()" << endl;}
};
int main()
{ Person p1;//基类对象Student s1;//派生类对象return 0;
}
- Person类中有两个虚函数,一个非虚函数。
- Student继承了Person类,并且重写了Func函数。
- 实例化出基类和派生类对象 监视窗口如下

可以发现: - 派生类对象也有一个虚表指针,虚表由两部分组成,一部分是继承基类的成员,另一部分是自己的成员。
- 基类对象和派生类对象的虚表是不一样的,派生类重写了基类的Func虚函数,所以派生类对象虚表中存的是派生类重写后的函数地址。所以虚函数的重写也覆盖,覆盖值得是虚表中虚函数的覆盖,重写是语法的叫法,覆盖是底层的原理。
- Func2虚函数被继承下来后也会被放到虚表中,Func3也会被继承下来,但是Func3不是虚函数,所以不会放到虚表中
多态的原理
class Person {
public://虚函数virtual void BuyTicket(){cout << "买票-全价" << endl;}
};
class Student : public Person
{//重写基类函数virtual void BuyTicket(){cout << "买票-半价" << endl;}
};
//必须是基类对象指针或引用调用
void Func(Person& people)
{people.BuyTicket();
}
void Test()
{Person p1;Func(p1);Student s1;Func(s1);
}
对于上面的例子

- 当people指向的是基类对象时,people.BuyTicket();就会在基类对象p1中的虚表中找到对应虚函数
- 当people指向的是派生类对象时,,people.BuyTicket();就会在派生类对象s1中的虚表中找到对应的虚函数
- 通过虚表,实现了了不同对象去完成同一行为时,展现出不同的形态。
单继承和多继承关系的虚函数表
单继承中的虚函数表
class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int _b;
};
int main()
{Base b1;Derive d1;return 0;
}
- 上面代码中基类Base有两个虚函数
- 派生类Derive继承了Base类,并且重写了func1函数,且新增了两个虚函数func3 和func4
单继承对象模型:


通过监视窗口发现:派生类中新增的虚函数func3 和func4 没有进虚函数表。(不知道是编译器故意的 还是编译器的
bug)

我们可以利用程序自己打印虚表来观察,参考代码如下。
class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
typedef void (*VF_Ptr)();//函数指针
//VF_Prt table[];//函数指针数组//打印虚函数表
void PrintVFTable(VF_Ptr table[])
{for (int i = 0; table[i] != nullptr; ++i){printf("table[%d] = %p\n", i, table[i]);VF_Ptr Fun = table[i];//取出函数地址对其进行访问Fun();}cout << endl;
}
int main()
{Base b1;Derive d1;//虚函数表指针在对象的头四个字节(32位下), 拿到对象的地址对其强制类型转换:(int*)&p1//在解引用就能拿到对象前四个字节地址:*((int*)&p1),在将其强制类型转换位函数指针:(VF_Ptr*)(*(int*)&p1)PrintVFTable((VF_Ptr*)(*(int*)&b1));PrintVFTable((VF_Ptr*)(*(int*)&d1));return 0;
}
运行结果:

可以看出,不论是派生类还是基类,只要是虚函数都会存到虚表中
多继承中的虚函数表
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int _a;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int _b;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int _d;
};
多继承对象模型:
多继承对象模型对比单继承模型就复杂很多



派生类会有两个虚表,监视窗口仍然无法观察, 通过程序打印查看
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;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);//派生类第二个虚表指针需要加行Base对象大小的偏移量才能获得VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}
结果如下

和上面的对象模型一样。
可以发现:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
相关文章:
C++ 多态以及多态的原理
文章目录 多态的概念多态的构成条件虚函数的重写虚函数重写的两个例外 重载、重写(覆盖)、重定义(隐藏)对比C11 final 和 override关键字抽象类接口继承和普通继承多态的原理虚函数表多态的原理 单继承和多继承关系的虚函数表单继承中的虚函数表多继承中的虚函数表 多态的概念 …...
贝蒂详解<string.h>(下)
✨✨欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:C语言学习 贝蒂的主页:Betty‘s blog 目录 1. 简介 2. memset()函数 2.1用法 2.2实例 2.3 实现me…...
问题 F: 分巧克力
题目描述 儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。小明一共有 N 块巧克力,其中第i 块HiWi 的方格组成的长方形。 为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。 切出的巧克力需要满足&am…...
安装pillow可能遇到的问题
安装命令 pip install Pillow安装 Pillow 这个 Python 图像处理库时可能会遇到多种问题。以下一些常见的安装问题及其解决方法: 缺少依赖项: Pillow 安装可能需要一些基础库,如 libjpeg 和 zlib。如果在安装时提示缺少这些库,你需要先安装它…...
详解ajax、fetch、axios的区别
众所周知它们都用来发送请求,其实它们区别还蛮大的。这也是面试中的高频题,本文将详细进行讲解。 1. ajax 英译过来是Aysnchronous JavaScript And XML,直译是异步JS和XML(XML类似HTML,但是设计宗旨就为了传输数据&a…...
致远OA getAjaxDataServlet XXE漏洞复现(QVD-2023-30027)
0x01 产品简介 致远互联-OA 是数字化构建企业数字化协同运营中台,面向企业各种业务场景提供一站式大数据分析解决方案的协同办公软件。 0x02 漏洞概述 致远互联-OA getAjaxDataServlet 接口处存在XML实体注入漏洞,未经身份认证的攻击者可以利用此漏洞读取系统内部敏感文件…...
力扣最热一百题——只出现一次的数字
这个合集已经很久没有更新了,今天来更新更新~~~ 目录 力扣题号 题目 题目描述 示例 提示 题解 Java解法一:Map集合 Java解法二:位运算 C位运算代码 力扣题号 136. 只出现一次的数字 - 力扣(LeetCode) 下述题…...
UE5 UE4 修复GPU驱动程序崩溃
原贴链接:https://mp.weixin.qq.com/s/e5l9XtfwEFWgwhHi1b2idg UE5 UE4在处理含有大量图形的项目时,你有可能会遇到GPU崩溃 可以通过修改注册表,修复崩溃。 GPU崩溃情况概述 UE5 UE4在处理含有大量图形的项目时,你有可能会遇到G…...
SpiderFlow爬虫平台 前台RCE漏洞复现(CVE-2024-0195)
0x01 产品简介 SpiderFlow是新一代爬虫平台,以图形化方式定义爬虫流程,以流程图的方式定义爬虫,不写代码即可完成爬虫,是一个高度灵活可配置的爬虫平台。 0x02 漏洞概述 SpiderFlow爬虫平台src/main/java/org/spiderflow/controller/FunctionController.java文件的Functi…...
帆软report 设置条件属性,值为负数标为红色功能时,不生效
详细情况: 在设置负数为红色功能前,已经有一个条件属性,数据集获取的值为空或者为0时,转换成 - 符号。如下图: 具体表单显示效果如下: 条件属性2设置 原因 因为条件属性1设置的 - 符号没有设置颜色…...
QML实现的图片浏览器
很久之前实现了一个QWidget版本的图片浏览器:基于Qt5的图片浏览器QHImageViewer 今天用QML也实现一个,功能差不多: ●悬浮工具栏 ●支持图片缩放、旋转、还原、旋转、拖动。 ●拖动图片时,释放鼠标图片会惯性滑动。 ●支持左右翻页查看文件夹中的图片。 ●支持保存图片至本…...
【HTML】对字体的所有操作详解(经典)
目录 一、文字样式设置的基本标签二 、 设置文字的颜色三、设置文字的尺寸四、 设置文字的字体五、 使文字倾斜六、 使文字加粗七、处理网页中的特殊字符十、 如何更方便地忽略浏览器对部分HTML的解析十一、 其他文字修饰方法十二、为了让文字富有变化,或者为了着意…...
关于调查项目的讨论
怎么安排一个调查项目 要安排一个调查项目,你需要经过以下步骤: 1. 确定调查目的:明确你为什么要进行这个调查,你想了解什么问题或获得什么信息。 2. 制定研究问题:根据调查目的,确定需要回答的具体问题…...
Matlab三维绘图
绘制三维图plot3 t0:pi/50:10*pi; xsin(t); ycos(t); zt; plot3(x,y,z); 产生栅格数据点meshgrid 这个接口在绘制三维图像里面相当重要,很多时候要将向量变成矩阵才能绘制三维图。 x0:0.5:5; y0:1:10; [X,Y]meshgrid(x,y); plot(X,Y,o); x和y是向量,…...
一体式气象站的优点是什么?带大家了解一下
一体式气象站是一款高度集成、低功耗、可快速安装、便于野外监测使用的高精度自动气象观测设备。 一体式气象站的优点主要体现在以下几个方面: 集成度高:一体式气象站集成了多种气象传感器、数据处理单元、显示单元和通讯模块等,可以同时监…...
第八讲_css定位
css定位 1. css定位介绍2. 静态定位(static)3. 相对定位(relative)4. 绝对定位(absolute)5. 固定定位(fixed)6. 粘性定位(sticky) 1. css定位介绍 在 css 中…...
找出字符串中第一个匹配项的下标(Leetcode28)
例题: 分析: 题目的意思就是: 先给出一个字符串pattern,要拿着pattern字符串和原始字符串(origin)比对,若在origin中找到了pattern字符串,则返回pattern字符串在原始字符串origin中的…...
【分布式微服务专题】从单体到分布式(四、SpringCloud整合Sentinel)
目录 前言阅读对象阅读导航前置知识一、什么是服务雪崩1.1 基本介绍1.2 解决方案 二、什么是Sentinel2.1 基本介绍2.2 设计目的2.3 基本概念 三、Sentinel 功能和设计理念3.1 流量控制3.2 熔断降级3.3 系统负载保护 四、Sentinel 是如何工作的 笔记正文一、简单整合Sentinel1.1…...
RHCE9学习指南 第19章 网络时间服务器
19.1 时间同步的必要性 对于一些服务来说对时间要求非常严格,例如,图19-1所示由三台服务器搭建的ceph集群。 图19-1 三台机器搭建的集群对时间要求比较高 这三台服务器的时间必须要保持一样,如果不一样,就会显示报警信息。那么…...
大模型 RAG 问答技术架构及核心模块盘点:从 Embedding、prompt-embedding 到 Reranker
对于RAG而言,2023年已经出现了很多工作,草台班子有了一堆,架构也初步走通,2024年应该会围绕搜索增强做更多的优化工作。 因此我们今天来系统回顾下RAG中的模块,包括一些架构,文本嵌入embedding等ÿ…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...
