总结虚函数表机制——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.rhkb.cn/news/285910.html

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

相关文章

【MySQL】InnoDB引擎

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

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…

iOS开发之SwiftUI

iOS开发之SwiftUI 在iOS开发中SwiftUI与Objective-C和Swift不同&#xff0c;它采用了声明式语法&#xff0c;相对而言SwiftUI声明式语法简化了界面开发过程&#xff0c;减少了代码量。 由于SwiftUI是Apple推出的界面开发框架&#xff0c;从iOS13开始引入&#xff0c;Apple使用…

成为创作者的第 730 天——创作纪念日

​​ 文章目录 &#x1f4e8; 官方致信&#x1f3af;我的第一篇文章&#x1f9e9; 机缘与成长 &#x1f3af; 成就&#x1f3af; 目标 &#x1f4e8; 官方致信 今天早上打开 CSDN 私信一看&#xff0c;看到了这一条消息&#xff0c;然后看了下日期。突然感慨到&#xff0c;是…

基于NetCoreServer的WebSocket客户端实现群播(学习笔记)

一、NetCoreServer介绍 超快速、低延迟的异步套接字服务器和客户端 C# .NET Core 库&#xff0c;支持 TCP、SSL、UDP、HTTP、HTTPS、WebSocket 协议和 10K 连接问题解决方案。 开源地址&#xff1a;https://github.com/chronoxor/NetCoreServer 支持&#xff1a; Example: TC…

Java中的代理模式(动态代理和静态代理)

代理模式 我们先了解一下代理模式&#xff1a; 在开发中&#xff0c;当我们要访问目标类时&#xff0c;不是直接访问目标类&#xff0c;而是访问器代理类。通过代理类调用目标类完成操作。简单来说就是&#xff1a;把直接访问变为间接访问。 这样做的最大好处就是&#xff1a…

基于Spring Boot网络相册设计与实现

摘 要 网络相册设计与实现的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&am…

在微信小程序中或UniApp中自定义tabbar实现毛玻璃高斯模糊效果

backdrop-filter: blur(10px); 这一行代码表示将背景进行模糊处理&#xff0c;模糊程度为10像素。这会导致背景内容在这个元素后面呈现模糊效果。 background-color: rgb(255 255 255 / .32); 这一行代码表示设置元素的背景颜色为白色&#xff08;RGB值为0, 0, 0&#xff09;&a…

第八节:深入讲解SMB中的Http组件

一、概述 Http组作是SMB中的核心组件之一&#xff0c;在第七节中讲解了如何简洁的进行web程序部署和运行&#xff0c;这只是它的功能之一。在本节中&#xff0c;我们将介绍Http组件的重要属性。 二、请求头Request 1、支持方法 支持POST、GET、PUT、DELETE、OPTIONS等方法&a…

uniapp ios证书失效

前面是按照网上查找的方法 作者大大的地址 1、一个ios账户&#xff08;688付费版&#xff09; 2、登录 Apple Developer 3、创建Identifiers ps&#xff1a;创建时需继承苹果的sdk&#xff0c;只需要一个就行 点击continue再点击Register即可 4、创建.cer证书 &…

DP:斐波那契数列模型

创作不易&#xff0c;感谢三连支持 &#xff01; 斐波那契数列用于一维探索的单峰函数之中&#xff0c;用于求解最优值的方法。其主要优势为&#xff0c;在第一次迭代的时候求解两个函数值&#xff0c;之后每次迭代只需求解一次 。 一、第N个泰波那契数 . - 力扣&#xff08;…

AnyGo for Mac最新激活版:位置模拟软件打破地域限制

AnyGo for Mac&#xff0c;一款专为Mac用户打造的位置模拟软件&#xff0c;让您能够轻松打破地域限制&#xff0c;畅享无限可能。 软件下载&#xff1a;AnyGo for Mac v7.0.0最新激活版 通过AnyGo&#xff0c;您可以随时随地模拟出任何地理位置&#xff0c;无论是国内热门景点还…

Word2vec学习笔记

&#xff08;1&#xff09;NNLM模型&#xff08;神经网络语言模型&#xff09; 语言模型是一个单纯的、统一的、抽象的形式系统&#xff0c;语言客观事实经过语言模型的描述&#xff0c;比较适合于电子计算机进行自动处理&#xff0c;因而语言模型对于自然语言的信息处理具有重…

【Unity】uDD插件抓屏文字显示不清晰怎么办?

【背景】 之前介绍过用一款简称uDD&#xff08;uDesktopDuplication&#xff09;的开源插件抓取电脑桌面。整体效果不错&#xff0c;看电影很流畅。但是当切换到文档&#xff0c;或者仔细看任何UI的文字部分时&#xff0c;发现就模糊了。 【分析】 由于是依托于Canvas上的Te…

【vue3学习之路(一)】

文章目录 前言一、vue3项目创建1.1环境准备1.1.1 基于 vue-cli 创建&#xff08;脚手架创建&#xff09;1.1.2 基于 vite 创建&#xff08;推荐&#xff09; 二、熟悉流程总结 前言 参考视频&#xff1a;https://www.bilibili.com/video/BV1Za4y1r7KE?p10&spm_id_frompag…

怎么压缩动图的大小?gif图片过大怎么压缩?动图压缩不求人

工作中&#xff0c;大家都可能会遇到处理GIF图片的情况。 比如&#xff1a; 1.网站 网页上如果有一大堆图片&#xff0c;一定要处理动图&#xff08;它容量太大了&#xff09;。 因为这玩意多了很卡&#xff0c;使网页加载速度慢。 2.公众号 公众号上传动图和整篇推文是有…

4.1.1 SN74LVC245A型总线收发器

SN74LVC245A是德州仪器(Texas Instruments)推出的一款集成电路芯片,属于SN74系列。它是一款双向总线驱动器,可用于高速CMOS逻辑电平之间的电平转换。这款芯片可以实现3.3V/5V逻辑电平之间的转换,具有高速和低功耗的特点。SN74LVC245A在电子系统中常用于数据总线的电平转换…

QT_day3:2024/3/22

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

Python库xarray:强大的多维数据处理工具

Python库xarray&#xff1a;强大的多维数据处理工具 在数据科学和科学计算领域&#xff0c;处理多维数据是一项常见而重要的任务。Python库xarray是一个功能强大的工具&#xff0c;专门用于处理、分析和可视化多维数据集。本文将深入介绍xarray库的特性、用法和优势&#xff0c…

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

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