当前位置: 首页 > news >正文

总结虚函数表机制——c++多态底层原理

        前言: 前几天学了多态。 然后过去几天一直在测试多态的底层与机制。今天将多态的机制以及它的本质分享给受多态性质困扰的友友们。

        本节内容只涉及多态的原理, 也就是那张虚表的规则,有点偏向底层。 本节不谈语法!不谈语法!不谈语法!想要学习语法的话本节并不合适。

虚函数表

        一个类中, 如果包含虚函数成员, 那么这个类进行实例化的对象中,会出现一张虚函数表 

这个概念很重要,只要包含虚函数成员, 就有虚函数表!

                            只要包含虚函数成员, 就有虚函数表!

                            只要包含虚函数成员, 就有虚函数表!

通过调试下图代码进行观察:

class A
{
public:A(){func();cout << "A()" << endl;}virtual void func(){cout << "Afunc()" << endl;}void test(){func();}private:int a = 0x11;//这里初始化使用十六进制是为了好观察
};int main() 
{A a;return 0;
}

 

绿框框就是A类型的那张虚函数表。 注意:A类的任何实例化对象的虚函数表是同一个或着多个。

虚函数表如何生成

        虚函数表的本质是一个函数指针数组。 他被存放在常量区

编译器通过编译, 就能分析整个类中的函数成员是否存在虚函数。 从而向常量区申请一块内存用来存放虚函数地址的地址, 这也就是我们所说的虚函数表。所以, 虚函数表是在编译期间生成的。

        虚函数表是存放函数指针的函数指针数组,所以这张虚函数表的类型是函数指针的指针也就是二级指针, 然后我们类实例化对象要保存这么一张虚函数表就需要用到一个三级指针指向这张虚函数表。 是不是感觉很恶心?不过没关系, 这一切都是编译器的工作, 我们只需要了解原理即可。像上图中的A类的实例化对象a, 我们现在观察下它的内存:

ps: 因为不小心将对象a和成员变量名重复了, 下面将会令成员变量名变成_a;

        好了, 现在我们知道了虚函数表是在什么时候生成的。 那么虚函数表的指针_vfptr是什么时候加入到对象的成员变量中的呢?下面我们进行进行观察。 

        现在对象a刚刚定义。虽然成员变量还没有初始化,但是对象a中已经有了虚函数表的指针_vfptr, 只是没有初始化。 所以, 我们就可以下结论:如果一个类含有虚函数表, 那么这个类的实例化对象,会在生成的一瞬间增加虚函数表指针变量。但是这一个或者多个虚函数指针变量并没有进行初始化。也就是说, 如果类中含有虚函数, 那么这个类中默认存在一个_vfptr指针变量。(这里可能增加多个虚函数表指针, 这个涉及继承, 也是多态的本质。后续讨论)。

        然后我们接着进行调试, 观察这个虚函数表指针什么时候进行初始化 

进入构造函数, 没有变化。 继续调试。

        变了。 当我们调试到初始化列表的时候, 对象a的虚函数表进行了初始化。 这里第二个结论就出来了:对象的虚函数表会在构造函数的初始化列表进行初始化。也就是说, _vfptr指针和其他的成员变量一样, 都是在初始化列表进行初始化。

通过上面的分析,我们基本可以总结一下:虚函数列表是在编译期间进行生成的。 它存放在常量区而虚函数列表的指针_vfptr会和类的其他成员变量一样。 实例化瞬间声明变量, 初始化列表初始化变量。

继承与重写

        多态在语法规则上, 说了多态的形成条件:1、虚函数的重写;

                                                                             2、父类对子类的引用或者父类的指针指向子类。

重写

        现在, 我们来讨论虚函数的重写:

        虚函数在什么情况下会有重写的概念?当一个父类存在虚函数。 并且子类同样有函数名称相同的函数(这个时候不管这个函数是不是虚函数, 都会被编译器默认识别成虚函数)的时候就会有重写的概念。这里的虚函数名称相同是指函数名称相同,函数参数相同,返回值相同。  

        重写必须有相同函数名称的虚函数的继承。继承后子类的虚函数列表之中不会再有父类的相应的虚函数。只存在重写的虚函数。但是不影响父类。 

        如图:


class A
{
public:virtual void func(int a = 1){cout << "Afunc():" << a << endl;}private:int _a = 0x11;//这里初始化使用十六进制是为了好观察
};class C : public A
{
public:virtual void func(int a = 3)//该func重写了父类的func. 所以C类的虚函数列表之中不会包含父类的func,只有自己这个重写了的func。{cout << "Cfunc()" << a << endl;}int c = 0x11;
};int main()
{C cc;A* ptr = &cc;ptr->func();cc.func();return 0;
}

        这里子类的func重写了父类的func. 所以C类的虚函数列表之中不会包含父类的func,只有自己这个重写了的func。(这条重写性质非常重要! 后续多态的形成会用到!)

我们观察一下vs:

        通过观察右边的内存窗口,我们可以看到C的实例化对象cc之中的虚函数表中只有一个 函数的地址。这里的00 00 00 00就是虚函数列表结尾的标志。 相当于字符串的末尾斜杠零)。

        

虚函数表的继承

       但是图中, 现在我们还可以看到另外一个现象。 那就是为什么cc的虚函数指针跑到了继承的A类的虚函数列表里?

        这里是虚函数列表继承的规则。

         如果是单继承。那么子类的虚函数的地址和继承来的父类的虚函数的地址都会存放在一张虚函数列表之中。并且这张虚函数列表之中父类的虚函数在低地址。 子类的虚函数在高地址。

        如下是一个单继承。


class A
{
public:virtual void func(int a = 1){cout << "Afunc():" << a << endl;}private:int _a = 0x11;//这里初始化使用十六进制是为了好观察
};class C : public A
{
public:virtual void func(int a = 3){cout << "Cfunc()" << a << endl;}virtual void func2(){cout << "func2()" << endl;}int c = 0x11;
};int main()
{C cc;A* ptr = &cc;ptr->func();cc.func();return 0;
}

        在C类中, C只继承了A类, 所以是单继承。  C类之中只有一张虚函数列表。 这张虚函数列表之中包含了C重写的虚函数func()和自己本身的虚函数func2().我们看一下内存图:

        这里需要注意的是不要只观察监视窗口绿色箭头所指向的地方。 因为监视窗口有些时候是不准的, 就像这个时候就不准。 我们需要看一下内存窗口红色箭头指向的地方。 这里是真正的底层内存。 我们可以发现C类的实例化对象中只有一张虚函数列表, 并且这张虚函数列表之中有两个函数指针。 一个是重写的父类的func函数指针。 一个是自己的func2函数指针。 (其实这里我总结了一个结论:任意的虚函数表之中只有三类虚函数指针,第一种是继承的父类的并且重写了的虚函数指针, 第二种是继承的父类的没有被重写的虚函数指针, 第三种是自己的虚函数指针。C类虚函数表中包含的指针式第一种和第三种)

        现在看多继承。

        如果是多继承,假如继承了n个父类。那么子类会有n张虚函数表。 子类的虚函数地址和第一个继承的类的虚函数地址存放在一张虚函数表之中,地址存放规则同单继承。其他虚函数表各自存放继承来的父类的虚函数或者重写的虚函数。   这里要注意的是, 子类对象的这些虚函数表与父类的虚函数列表不是同一张, 但是如果虚函数没有被子类重写, 那么里面存放的虚函数地址是相同的。

这里测试如下代码


class A
{
public:virtual void func(int a = 1){cout << "Afunc():" << a << endl;}virtual void funcA() {cout << "func()A" << endl;}private:int _a = 0x33;//这里初始化使用十六进制是为了好观察
};class B
{
public:virtual void funcB(){cout << "func()B" << endl;}private:int b = 0x22;
};class C : public A, public B
{
public:virtual void func(int a = 3){cout << "Cfunc()" << a << endl;}virtual void funcC(){cout << "func()C" << endl;}int c = 0x11;
};int main()
{C cc;A* ptr = &cc;ptr->func();cc.func();return 0;
}

如图是C类继承A类和B类之后的实例化对象cc中的虚函数表情况。 可以看到,cc中有两张虚函数表。 一张存放在A类的板块, 一张存放在B类的板块。(这里为什么要分板块, 是因为要切片。 当我们使用父类的引用引用子类对象或者父类指针解引用子类对象的时候,我们能拿到的就是相应的为继承来的变量开辟的板块空间) 

现在我们就来观察底层内存空间。

                  

        这时C, B, A类的成员变量。 我们通过对他们进行缺省值初始化。 为了将他们每个板块区分出来。 现在我们开始观察空间。 

        对cc取地址, 观察cc的空间

 

这里红色箭头就是子类C本身的变量_c, 绿色箭头就是继承的B类的变量_b, 蓝色箭头就是继承的A类的变量_a。

所以这里我们就能判断, A类的内存板块就是前八个字节。B类的内存板块是中间八个字节。然后最后四个字节是C类本身的成员变量c的内存空间。


多态的形成

        现在我们来重新回顾一下语法上多态的形成条件:

        1、虚函数的重写

        2、父类对子类的引用或者父类的指针对子类的解引用

        现在虚函数的重写上面我们已经进行了分析。 现在我们来分析第二条内容。

        当我们进行父类对子类的引用或者父类的指针对子类进行解引用的时候, 这时候其实会发生切片原理。

        如图利用A*的ptr指针解引用一个C类的实例化对象相当于只拿到了图中横线上面的部分。

        我们已经知道, ptr解引用后拿到的其实是C类实例化对象之中属于A类的那一块内存。这块内存中的虚函数表之中的虚函数就是继承而来的A类的虚函数。 (如果有函数被重写, 还要将相应的虚函数进行替换。 如果忘记了这条性质, 请回看重写模块)

        这个时候如果, C类中继承A类而来的虚函数被重写时,并且我们恰好通过ptr调用了C这个被重写的虚函数, 这就是多态。
        也就是说我们使用A类的指针找到了C的实例化对象中的重写的继承A类而来的虚函数。这个结果其实和我们使用C类的实例化对象调用相应的虚函数的结果是一样的。

        这里重点就是切片原理和重写的性质。

最后还有另外一个要点:就是对于缺省值。

        对于多态的缺省值, 我们要特殊关照。

如果构成了多态, 那么这个时候调用的虚函数的缺省值应该是父类的缺省值。

        对于如下代码进行测试

class A
{
public:virtual void func(int a = 1){cout << "Afunc():" << a << endl;}virtual void funcA() {cout << "func()A" << endl;}private:int _a = 0x33;//这里初始化使用十六进制是为了好观察
};class C : public A
{
public:virtual void func(int a = 3){cout << "Cfunc()" << a << endl;}virtual void funcC(){cout << "func()C" << endl;}int c = 0x11;
};int main()
{C cc;A* ptr = &cc;ptr->func();return 0;
}

这是A类中的func

这是C类中的func

 请问。 测试中的代码, 打印结果是什么呢?

我们看一下vs的结果:

这里之所以不是Cfunc()3的原因是因为这里的缺省值使用的时父类的缺省值。  

        这里我们需要特殊记忆, 当构成多态的时候。 假如虚函数有缺省值, 那么这个缺省值时是父类的缺省值。 如果父没有缺省值, 子类有。 那么这个重写的虚函数没有缺省值。 注意, 这里是当构成多态的时候 !当构成多态的时候!当构成多态的时候!假如没有构成多态, 我直接使用C类对象调用重写的func函数, 那么结果就还剩Cfunc()3.

以上, 就是多态的底层原理。 

        

相关文章:

总结虚函数表机制——c++多态底层原理

前言&#xff1a; 前几天学了多态。 然后过去几天一直在测试多态的底层与机制。今天将多态的机制以及它的本质分享给受多态性质困扰的友友们。 本节内容只涉及多态的原理&#xff0c; 也就是那张虚表的规则&#xff0c;有点偏向底层。 本节不谈语法&#xff01;不谈语法&#x…...

Contos7 安装 Maven

Contos7 安装 Maven 前言 ​ Maven是一个用于构建和管理Java项目的强大工具。它提供了一种简单且一致的方式来构建、测试和部署项目&#xff0c;同时管理项目依赖关系。Maven基于项目对象模型&#xff08;Project Object Model&#xff0c;POM&#xff09;&#xff0c;使用XML…...

对适配器模式的理解

目录 一、适配器模式是什么&#xff1f;二、示例【[来源](https://refactoringguru.cn/design-patterns/adapter)】1 第三方依赖 &#xff08;接口A 数据A&#xff09;2 客户端3 方钉转圆钉的适配器&#xff08;数据B&#xff09; 三、适配器&#xff08;xxxAdapter&#xff0…...

纯前端调用本机原生Office实现Web在线编辑Word/Excel/PPT,支持私有化部署

在日常协同办公过程中&#xff0c;一份文件可能需要多次重复修改才能确定&#xff0c;如果你发送给多个人修改后再汇总&#xff0c;这样既效率低又容易出错&#xff0c;这就用到网页版协同办公软件了&#xff0c;不仅方便文件流转还保证不会出错。 但是目前一些在线协同Office…...

双指针的详细教程

双指针算法是一种常用的算法技巧&#xff0c;它通常用于在数组或字符串中进行快速查找、匹配、排序或移动操作。 双指针并非真的用指针实现&#xff0c;一般用两个变量来表示下标&#xff08;在后面都用指针来表示&#xff09; 双指针算法使用两个指针在数据结构上进行迭代&a…...

【Review+预测】测试架构演进的曲折之路

文章目录 前言 一、“原始”阶段 二、“小打小闹”阶段 三、“小米加步枪”阶段 四、“摩托化部队”阶段 五、“骑兵连”阶段 六、“海军陆战队”阶段 七、“社区型组织”阶段 前言 近期公司的测试团队需要重新组织安排&#xff0c;本着谦虚谨慎的态度&#xff0c;我从…...

2015年认证杯SPSSPRO杯数学建模D题(第二阶段)城市公共自行车全过程文档及程序

2015年认证杯SPSSPRO杯数学建模 D题 城市公共自行车 原题再现&#xff1a; 城市交通问题直接影响市民的生活和工作。在地形平坦的城市&#xff0c;公共自行车出行系统是一种很好的辅助手段。一般来说&#xff0c;公共自行车出行系统由数据中心、驻车站点、驻车桩、自行车&…...

视频汇聚平台EasyCVR启用图形验证码之后调用login接口的操作方法

视频综合管理平台EasyCVR视频监控系统支持多协议接入、兼容多类型设备&#xff0c;平台可以将区域内所有部署的监控设备进行统一接入与集中汇聚管理&#xff0c;实现对监控区域的实时高清视频监控、录像与存储、设备管理、云台控制、语音对讲、级联共享等&#xff0c;在监控中心…...

【数据结构】非线性结构——二叉树

文章目录 前言1.树型结构1.1树的概念1.2树的特性1.3树的一些性质1.4树的一些表示形式1.5树的应用2.二叉树 2.1 概念2.2 两种特殊的二叉树2.3 二叉树的性质2.4 二叉树的存储2.5 二叉树的基本操作 前言 前面我们都是学的线性结构的数据结构&#xff0c;接下来我们就需要来学习非…...

数据分析POWER BI之power query

1.导入数据 ctrla全选--数据--获取数据--其他来源--来自表格/区域 导入数据&#xff0c;进入编辑模式 2.整理与清除 清除&#xff1a;删除所选列的非打印字符 转换--格式--清除 修整&#xff1a;删除前面和后面的空格 转换---格式---修整&#xff08;修整后前面后面的空格没有了…...

【c语言】详解操作符(上)

1. 操作符的分类 2. 原码、反码、补码 整数的2进制表示方法有三种&#xff0c;即原码、反码、补码 有符号整数的三种表示方法均有符号位和数值位两部分&#xff0c;2进制序列中&#xff0c;最高位的1位是被当做符号位其余都是数值位。 符号位都是用0表示“正”&#xff0c;用…...

VR全景展示:传统制造业如何保持竞争优势?

在结束不久的两会上&#xff0c;数字化经济和创新技术再度成为了热门话题。我国制造产业链完备&#xff0c;但是目前依旧面临着市场需求不足、成本传导压力加大等因素影响&#xff0c;那么传统制造业该如何保持竞争优势呢&#xff1f; 在制造行业中&#xff0c;VR全景展示的应用…...

2.7、创建列表(List)

概述 列表是一种复杂的容器&#xff0c;当列表项达到一定数量&#xff0c;内容超过屏幕大小时&#xff0c;可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集&#xff0c;例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求&#xff08;如通讯录、…...

solr functionquery函数查询自定义函数实现

Solr是一个开源的搜索平台&#xff0c;基于Apache Lucene库构建&#xff0c;主要用于提供全文搜索的功能。它被设计为一个高度可靠、可扩展的搜索应用服务器。以下是Solr的一些主要使用场景&#xff1a; 全文搜索&#xff1a;Solr最核心的功能是提供全文搜索&#xff0c;它可以…...

如何将 Parallels虚拟机 安装或者迁移到 移动硬盘 or U盘?

本文讨论主题 如何安装 Parallels 虚拟机到移动硬盘和U盘? 已经安装到了mac上的虚拟机如何迁移到移动硬盘活着U盘上? 关于Parallels Deskshop 19 虚拟机 安装激活,文末关注公众号AIshape,回复:PD 进行获取通过将虚拟机安装在外置的移动硬盘U盘上,可以节省mac本身SSD的容…...

大型网站集群管理负载均衡

课程介绍 结合企业大规模应用&#xff0c;解决应用高并发问题&#xff0c;解决单节点故障问题&#xff0c;缓存数据库的应用。学完掌握知识点&#xff1a;企业应用实现四七层负载均衡&#xff0c;以及Nginx等应用的高可用性&#xff0c;Redis缓存数据库的部署应用以及高可用方…...

JAVA使用POI实现Excel单元格合并-02

JAVA使用POI实现Excel单元格合并 实现效果 解释&#xff1a;只要是遇见与前一行相同的数据就合并 引入jar <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.2</version></depe…...

深入了解 Linux 中的 MTD 设备:/dev/mtd* 与 /dev/mtdblock*

目录 前言一、什么是MTD子系统&#xff1f;二、 /dev/mtd* 设备文件用途注意事项 三、/dev/mtdblock* 设备文件用途注意事项 三、这两种设备文件的关系四、关norflash的一些小知识 前言 在嵌入式Linux系统的世界里&#xff0c;非易失性存储技术扮演着至关重要的角色。MTD&#…...

2、Spring CLI安装

安装 Spring CLI 提供了多种格式,让您选择自己喜欢的安装方法。可下载的制品可从发布页面获取。 二进制发行版 WindowsLinuxMac手动安装(Windows,其他自己看) spring-cli-standalone-<version>-windows.x86_64.zip - 打包了 x86 JDKspring-cli-installer-<versi…...

数据库备份工具(实现数据定时覆盖)

数据库备份工具&#xff08;实现数据定时覆盖&#xff09; 永远热爱&#xff0c;永远执着&#xff01; 工具介绍 自动化测试数据库更新调度程序 这段 Python 脚本自动化了每天定时从生产数据库更新测试数据库的过程。它利用了 schedule 库来安排并执行每天指定时间的更新任务…...

Spring Boot统一功能处理深度解析

第一章&#xff1a;为什么需要统一功能处理&#xff1f; 想象你正在开发一个电商系统&#xff0c;包含用户管理、商品管理、订单管理等模块。每个模块都需要&#xff1a; 用户身份验证操作日志记录异常统一处理数据格式标准化 如果每个模块都单独实现这些功能&#xff1a; …...

设计模式——面向对象设计六大原则

摘要 本文详细介绍了设计模式中的六大基本原则&#xff0c;包括单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖倒置原则和合成复用原则。每个原则都通过定义、理解、示例三个部分进行阐述&#xff0c;旨在帮助开发者提高代码的可维护性和灵活性。通过具体代码…...

springboot实现查询学生

文章目录 数据库前端 请求mybatis 数据库 前端 请求 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body> <a href"/list">启动学生管理…...

破局新能源消纳难题!安科瑞智慧能源平台助力10KV配电网重构未来

一、政策驱动&#xff1a;新型配电网迎来 “智慧化” 刚需 随着分布式光伏、工商业储能、电动汽车充电桩等新型电力设施大规模并网&#xff0c;传统 10kV 配电网正面临 “高渗透、强波动、多交互” 的运行挑战。2025 年 6 月 1 日正式实施的《配电网通用技术导则》&#xff08;…...

STM32 智能小车项目 两路红外循迹模块原理与实战应用详解

在嵌入式系统、机器人、智能设备等场景中&#xff0c;红外反射型光电传感器 被广泛应用于黑白识别、障碍检测、物体计数、位置判断等任务。其中&#xff0c;RPR220 是一款性能稳定、体积小巧的红外光电收发管&#xff0c;本文将详细介绍其工作原理、引脚参数、接线说明以及典型…...

洛谷题目:P2761 软件补丁问题 (本题简单)

个人介绍: 题目传送门: P2761 软件补丁问题 - 洛谷 (luogu.com.cn) 前言: 这道题是一个典型的状态搜索问题,核心目标就是利用给定d额多个补丁程序,将包含若干错误的软件修复成没有错误的状态,并且要使得修复过程当中的总耗时最少。下面是小亦为大家阐述滴思路: 1、状态…...

【Oracle】锁

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 锁基础概述1.1 锁的概念与作用1.2 锁的工作原理1.3 Oracle锁的分类 2. 行级锁 (Row-Level Locks)2.1 行级锁的基本概念2.1.1 TX锁&#xff08;事务锁&#xff09;2.1.2 行级锁的工作机制 2.2 行级锁的类型2.…...

UDP/TCP协议全解

目录 一. UDP协议 1.UDP协议概念 2.UDP数据报格式 3.UDP协议差错控制 二. TCP协议 1.TCP协议概念 2.三次握手与四次挥手 3.TCP报文段格式&#xff08;重点&#xff09; 4.流量控制 5.拥塞控制 一. UDP协议 1.UDP协议概念 当应用层的进程1要向进程2传输报文&#xff…...

视频自动化分割方案:支持按时间与段数拆分

在日常视频处理任务中&#xff0c;如何快速将一个较长的视频文件按照指定规则拆分为多个片段&#xff0c;是许多用户都会遇到的问题。尤其对于需要批量处理视频的开发者、自媒体运营者或内容创作者来说&#xff0c;手动剪辑不仅效率低下&#xff0c;还容易出错。这是一款绿色免…...

二叉树-104.二叉树的最大深度-力扣(LeetCode)

一、题目解析 这里需要注意根节点的深度是1&#xff0c;也就是说计算深度的是从1开始计算的 二、算法原理 解法1&#xff1a;广度搜索&#xff0c;使用队列 解法2&#xff1a;深度搜索&#xff0c;使用递归 当计算出左子树的深度l&#xff0c;与右子树的深度r时&#xff0c;…...