总结虚函数表机制——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.

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

        

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

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

相关文章

发布文章积分自动增加

controller ApiOperation(value "添加文章")PostMapping("/addwengzhang")public String addwengzhang(RequestBody WengDto wengDto) {if (wengDto.getContent() null || wengDto.getTitle() null) {return "参数不可为空";}User user user…

【MySQL】InnoDB引擎

逻辑结构 InnoDB存储引擎逻辑结构如图所示&#xff1a; Tablespace&#xff1a;表空间&#xff0c;一个数据库可以对应多个表空间。数据库中的每张表都有一个表空间&#xff0c;用来存放表记录、索引等数据。 Segment&#xff1a;段&#xff0c;表空间中有多个段&#xff0c…

第九届蓝桥杯大赛个人赛省赛(软件类)真题C 语言 A 组-乘积尾零

solution 找末尾0的个数&#xff0c;即找有多少对2和5 >问题等价于寻找所给数据中&#xff0c;有多少个2和5的因子&#xff0c;较少出现的因子次数即为0的个数 #include <iostream> using namespace std; int main() {// 请在此输入您的代码printf("31");…

Java代码基础算法练习-搬砖问题-2024.03.25

任务描述&#xff1a; m块砖&#xff0c;n人搬&#xff0c;男搬4&#xff0c;女搬3&#xff0c;两个小孩抬一砖&#xff0c;要求一次全搬完&#xff0c;问男、 女、小孩各若干&#xff1f; 任务要求&#xff1a; 代码示例&#xff1a; package M0317_0331;import java.util.S…

二手车交易网站|基于JSP技术+ Mysql+Java+ B/S结构的二手车交易网站设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

快速熟悉ElasticSearch的基本概念

1.全文检索 全文检索是通过文本内容进行全面搜索的技术。通过全文检索可以快速地在大量文本数据中查找包含特定关键词或者短语的文档&#xff0c;并且返回相关的搜索结果。 检索和查询的区别 检索没有搜索条件边界&#xff0c;检索的结果取决于相关性&#xff0c;相关性计算…

VUE:内置组件<Teleport>妙用

一、<Teleport>简介 <Teleport>能将其插槽内容渲染到 DOM 中的另一个位置。也就是移动这个dom。 我们可以这么使用它: 将class为boxB的盒子移动到class为boxA的容器中。 <Teleport to".boxA"><div class"boxB"></div> &…

OC高级编程 第3章:Grand Central Dispatch

3.1 Grand Central Dispatch (GCD)概要 3.1.1什么是GCD Grand Central Dispatch&#xff08;GCD&#xff09;是异步执行任务的技术之一。一般将应用中记述线程管理用的代码在系统级中实现。开发者只要定义想执行的任务并追加到Dispatch Queue中&#xff0c;GCD就能生成必要的…

如何使用Docker安装Paperless-ngx系统并实现远程在线搜索查阅文档

文章目录 1. 部署Paperless-ngx2. 本地访问Paperless-ngx3. Linux安装Cpolar4. 配置公网地址5. 远程访问6. 固定Cpolar公网地址7. 固定地址访问 Paperless-ngx是一个开源的文档管理系统&#xff0c;可以将物理文档转换成可搜索的在线档案&#xff0c;从而减少纸张的使用。它内置…

登录注册界面

T1、编程设计理工超市功能菜单并完成注册和登录功能的实现。 显示完菜单后&#xff0c;提示用户输入菜单项序号。当用户输入<注册>和<登录>菜单序号时模拟完成注册和登录功能&#xff0c;最后提示注册/登录成功并显示注册信息/欢迎XXX登录。当用户输入其他菜…

Open CASCADE学习|将圆转换为NURBS曲线

NURBS曲线&#xff0c;全称非均匀有理B样条曲线&#xff08;Non-Uniform Rational B-Splines&#xff09;&#xff0c;是计算机图形学中用于表示几何形状的数学表示方法。它结合了非均匀B样条&#xff08;B-Splines&#xff09;和有理基函数&#xff08;Rational Basis Functio…

R语言迅速计算多基因评分(PRS)

Polygenic Risk Scores in R 最朴素的理解PRS&#xff1a; GWAS分析结果中&#xff0c;有每个SNP的beta值、se值、P值&#xff0c;因为GWAS分析中将SNP变为0-1-2编码&#xff0c;所以这些显著的SNP的beta值&#xff0c;就可以用于预测。 比如&#xff1a;GWAS分析中&#xf…

QT_day3:2024/3/22

作业1&#xff1a;设计界面 使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin…

【Java基础知识总结 | 第六篇】Java反射知识总结

文章目录 6.Java反射知识总结6.1概述6.1.1什么是反射&#xff1f;6.1.2为什么使用反射&#xff1f; 6.2反射的原理6.3反射的使用6.3.1获取类对象&#xff08;1&#xff09;通过具体类的类名获取&#xff08;2&#xff09;通过对象实例获取&#xff08;3&#xff09;通过class.f…

CSS设置移动端页面底部安全距离

env(safe-area-inset-bottom)是一个CSS属性值&#xff0c;用于设置底部安全距离。它表示使用环境变量来获取底部安全距离的值。当使用环境变量时&#xff0c;需要使用env()函数来引用具体的环境变量。例如&#xff1a; <style> .box{padding-bottom: env(safe-area-inse…

使用uniapp 的 plus.sqlite 操作本地数据库报错:::table xxx has no column named xxxx

背景&#xff1a; 1、使用uniapp 的 plus.sqlite 进行APP本地数据库操作 2、SQLite 模块用于操作本地数据库文件&#xff0c;可实现数据库文件的创建&#xff0c;执行SQL语句等功能。 遇到&#xff1a;在之前创建的表上进行新增字段的操作时候&#xff0c;出现问题&#xff1a…

机器学习(一)

经典定义: 利用经验改善系统自身的性能。 经典的机器学习过程: 基本术语: 数据集:训练集、测试集 示例、样例、样本 属性、特征:属性值 属性空间、样本空间、输入空间 特征向量 标记空间、输出空间 归纳偏好(偏置): 任何一个有效的机器学习算法必有其偏好 学习算法的…

代码随想录算法训练营第33天|1005.K次取反后最大化的数组和|134. 加油站|135. 分发糖果

代码随想录算法训练营第33天|1005.K次取反后最大化的数组和|134. 加油站|135. 分发糖果 1005.K次取反后最大化的数组和 本题简单一些&#xff0c;估计大家不用想着贪心 &#xff0c;用自己直觉也会有思路。 https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%…

力扣438. 找到字符串中所有字母异位词

Problem: 438. 找到字符串中所有字母异位词 文章目录 题目描述思路及解法复杂度Code 题目描述 思路及解法 1.编写辅助函数bool same(vector& need, vector& matched)&#xff1a; 1.1 以need为标准&#xff0c;循环对比need和matched的每一个位置的元素值是否相等 2.获…

由浅到深认识Java语言(19):String类

该文章Github地址&#xff1a;https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.c…