C++多态的底层原理

目录

1.虚函数表

(1)虚函数表指针

(2)虚函数表

2.虚函数表的继承--重写(覆盖)的原理

3.观察虚表的方法

(1)内存观察

(2)打印虚表

        虚表的地址

        函数

        传参

(3)虚表的位置

4.多态的底层过程

5.几个原理性问题

(1)虚表中函数是公用的吗?(2)为什么必须传入指针或引用而不能使用对象?

(3)为什么私有虚函数也能实现多态?

(4)VS中的虚表中存的是指令地址?

6.多继承中的虚表

7.总结

1.虚函数表

(1)虚函数表指针

首先我们在基类Base中定义一个虚函数,然后 观察Base类型对象b的大小:

class Base
{
public:virtual void Func1(){cout << "Func1" << endl;}virtual void Func2(){cout << "Func2" << endl;}void f(){cout << "f()" << endl;}
protected:int b = 1;char ch = 1;
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}

我们发现,如果按照对齐原则来计算b的大小时,得到的结果是8,而我们打印的结果是:

这说明带有虚函数的类所定义的对象中,除了成员变量之外还有其他的东西被加入进去了(成员函数默认不在对象内,在代码段)。

我们可以通过调试来观察b中的内容:

我们发现对象中多了一个——vfptr,即为虚函数表指针。简称为虚表指针。

(2)虚函数表

仍然看上图,我们发现虚函数表指针下方的两个地址,这两个地址分别对应的就是Base中两个虚函数的地址,构成了一个虚函数表。所以虚函数表本质是一个指针数组,数组中的每一个元素都是一个虚函数的地址。

VS2019封装更为严密,在底层的汇编代码中,虚函数表中的地址并不一定是虚函数的地址,可能存放的是跳转到虚函数的地址的指令的地址。这个在后面会加以演示。

因此当我们调用普通函数和虚表函数时,它们的本质是不同的:

 Base* bb = nullptr;bb->f();bb->Func1();

其中bb调用f()的过程没有发生解引用的操作,非虚函数在公共代码段中,直接对其进行调用即可。而bb调用Func1()的过程中,需要通过虚表指针类来找到Func1(),而拿到虚表指针,找到虚函数的地址,这个时候需要对这个地址进行解引用操作,而bb是空,因此程序会崩溃。

//类指针访问成员变量的时候,会解引用:因为成员变量在类中
//类指针访问成员函数的时候,不解引用:因为成员函数不在类中,具体位置在编译的时候确定(具体见C++类和对象

我们知道对象中只存储成员变量,成员函数存储在公共代码段中,其实虚函数也是一样存储在公共代码段,只不过寻找虚函数需要通过虚表来确定位置。普通函数在编译时直接就可以确定位置。

2.虚函数表的继承--重写(覆盖)的原理

还拿上一节中买票的例子举例,其中父类中有两个虚函数,子类重写了其中的一个,子类中还有字节的函数。

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}virtual void Func1(){cout << "Func1" << endl;}
protected:int _a;
};
class Student :public Person
{
public:virtual void BuyTicket(){cout << "半价" << endl;}virtual void Func2(){cout << "Func2" << endl;}
protected:int _b;
};int main()
{Person a;Student b;return 0;
}

我们可以通过调试来观察以下他们的虚表和虚表指针。

显然父类对象_vfptr[0]中存放的是BuyTicket的地址,_vfptr[1]中存放的是Func1()的地址。子类对象中_vfptr[0]中存放的是继承并重写的BuyTicket的地址,_vfptr[1]存放的是继承下来但没有进行重写的Func1()的地址。通过对比我们发现:对于没有进行重写的Func1()的地址,通过对比我们发现:对于没有进行重写的Func1()来说,子类中虚表中的地址和父类中的是一样的,可以说是直接拷贝下来的。而对于进行了重写的BuyTicket来说,子类中虚表的地址与父类中明显不一样,其实是在拷贝了父类的地址后又进行了覆盖的。因此重写在底层的角度来说又叫做覆盖。

同时我们又发现了一个问题,那就是子类对象的虚表中为什么没有些它自己的虚函数地址Func2()呢?其实是写了的,只不过通过VS的监视窗口并不能看到,我们可以通过内存来进行观察:

3.观察虚表的方法

(1)内存观察

我们可以通过观察内存来观察虚函数表的情况,这里观察的是父类对象,会发现在虚函数指针的地址存放的是父类中两个虚函数的地址。

我们也可以观察一下子类对象:

与父类对象中存储的相同,唯一有区别的地方就是紫色的部分,存放的其实是子类虚函数Func2()的地址。这说明Func2()也在虚表中只不过在监视窗口没有看不到而已。

(2)打印虚表

        虚表的地址

通过观察内存,对于单继承来说,我们只需要打印对象的首元素的地址即可找到虚表,并进行打印。

我们发现对象的前四个字节存储的就是虚表的地址。可以通过这一点来打印虚表。

我们关闭一下调试来重新写一下代码(关闭调试后在进行运行地址会发生变化但是规律是不变的)

typedef void(*vfptr)();
void Printvfptr(vfptr* table)
{for (int i = 0; table[i] != nullptr; i++){printf("%d:%p\n", i, table[i]);}cout << endl;
}
int main()
{Person a;Student b;Printvfptr((vfptr*)*(void**)&a);Printvfptr((vfptr*)*(void**)&b);return 0;
}

下面来解释以下如何打印的虚表,分为两部分,一部分是函数,一部分是传参:

        函数

首先我们明确,虚函数指针是一个函数指针,因此为了简便我们可以将函数指针重命名为vfptr。通过接收虚表指针,并以此打印指针数组中的内容(虚函数的地址)。

        传参

拿父类对象a举例,我们要找到a的前四个字节的内容,即虚表指针,然后再传入函数中。

首先使用(void**)对a的地址进行强制类型转换,这其中发生了切割。使用(void**)的原因在于,由于不知道是使用32位还是64位系统,但我们可以通过指针的大小来判断。首先将&a转换成一个指针,再将其转换成一个指针类型,再进行解引用就得到了a的前4或者8个字节。但同时我们需要传递的是一个vfptr类型的函数指针,所以还需要进行(*vfptr)类型的强制转换。

有了前面的解释,我们就可以理解打印虚表的原理了,我们把这一段代码运行一下:

发现分别打印出了a和b的虚函数表。

如果打印的虚函数数量不对,这是VS编译器的bug,我们可以重新生成解决方案,再重新运行代码。

(3)虚表的位置

我们还可以观察一下虚表的位置,在哪个区域:

使用其他区域的变量进行对比:

Person per;
Student std;
int* p = (int*)malloc(4);
printf("堆:%p\n", p);
int a = 0;
printf("栈:%p\n", &a);
static int b = 1;
printf("数据段:%p\n", &b);
const char* c = "aaa";
printf("常量区:%p\n", &c);
printf("虚表:%p\n", *(void**)&std);
return 0;

打印的结果是:

我们发现虚表的位置在数据段和常量区之间。大致属于数据段。

4.多态的底层过程

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}virtual void Func1(){cout << "Func1" << endl;}
protected:int _a;
};
class Student :public Person
{
public:virtual void BuyTicket(){cout << "半价" << endl;}virtual void Func2(){cout << "Func2" << endl;}
protected:int _b;
};
void F(Person& p)
{p.BuyTicket();
}
int main()
{Person per;Student std;F(per);F(std);return 0;
}

我们还使用这一段代码来举例,首先复习一下多态:使用父类的指针或引用去接收子类或者父类的对象,使用该指针或者引用调用虚函数,调用的是父类或子类中不同的虚函数。

下面来分析原理:

父类对象原理:

首先用父类引用p来接收父类对象per,此时p中的虚表和per中的虚表一摸一样,只需要访问_vfptr中的BuyTicket地址。此时的p不是新创建了一个父类对象,而是子类对象std切片后构成的,其中男就将重写之后的BuyTicket()的地址也随之切入了p。可以把怕堪称原std的包含_vfptr的一部分。

总结:基类的指针或者引用,指向谁就去谁的虚函数表中找到对应位置的虚函数进行调用。

5.几个原理性问题

了解了多态原理之后,就可以分析出在上一节中出现的一些现象规律。

(1)虚表中函数是公用的吗?

虚表中的函数和类中的普通函数一样是放在代码段的(虚表在数据段),只是虚函数还需要将地址存一份到虚表,方便实现多态。这也说明同一类型的不同对象的虚表指针是相同的,我们还可以通过调试观察:

	Person per;Person pper;


(2)为什么必须传入指针或引用而不能使用对象?

当我们使用父类对象去接收时,父类对象本身就具有一个虚表了,当子类对象传给父类对象的时候,其他内容会发生拷贝,但是虚表不会,C++这样处理的原因在于,如果虚表也会发生拷贝的化,那么该父类对象的虚表就存了子类对象的虚表,这是不合理的。

我们同样可以通过调试来进行观察:

void F(Person p)
{p.BuyTicket();
}
int main()
{Person per;Student std;F(std);
}

这是std中的虚表内容。

这是p中虚表内容,而且在调试过程中,程序是进入父类中进行调用函数的。

(3)为什么私有虚函数也能实现多态?

这是因为编译器调用了父类的public接口,由于是父类的引用或者指针,因此编译器 发现是public之后就不再进行检查了,只要在虚表中可以找到就能调用函数。

(4)VS中的虚表中存的是指令地址?

在VS2019中,为了封装严密,其实虚表中存入的是跳转指令,我们可以通过反汇编进行观察:

我们将虚表中的地址输入反汇编,看到的是这样的一条语句:

这是一条跳转指令,会跳转到BuyTicket()的实际地址处。

6.多继承中的虚表

谈到多继承就要谈到零星虚拟继承,这是一个庞大而复杂的问题,这里只介绍多继承中虚表的内容:

class Base1
{
public:virtual void Func1(){cout << "Func1" << endl;}virtual void Func2(){cout << "Func2" << endl;}
protected:int _a;
};
class Base2
{
public:virtual void Func3(){cout << "Func3" << endl;}virtual void Func4(){cout << "Func4" << endl;}
};
class Derive :public Base1, Base2
{
public:virtual void Func5(){cout << "Func5" << endl;}
};
int main()
{Derive a;
}

我们可以使用调试来观察a中的虚表内容:

通过调试我们可以看到a中有两个虚表指针分别存放的是Base1中的虚函数的地址和Base2中虚函数的地址,那么a中特有的类Func5()存放哪个虚表呢?这需要通过内存进行观察:

我们发现它被存放在了第一个虚表指针指向的虚表中。

我们知道打印第一个虚表指针指向虚表的方法,那么第二个虚表指针的该怎么处理呢:

Printvfptr((vfptr*)*(void**)((char*)&a+sizeof(Base1));

注意需要先将&a转换成char*类型,这样对其加一,才代表加一个字节。

7.总结

实际中我们不建议设定出菱形继承或者菱形虚拟继承,在实际中很少使用。

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

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

相关文章

SSM电子商务系统-计算机毕业设计源码68470

基于SSM框架的电子商务系统的设计与实现 摘 要 随着电子商务的迅猛发展和计算机信息技术的全面跃升&#xff0c;网上购物系统由于其迎合了人们诉求和期望而渗入社会生活各个层面和角落。本文设计并实现了一个基于SSM框架的电子商务系统。该系统旨在为用户提供一个舒适且快捷的…

Python基础——第一个Python程序

Python基础——第一个Python程序 一、编写和运行代码的工具1.1 为什么需要工具1.2 默认的交互式环境1.3 文本编辑神器 - Visual Studio Code1.4 专业的集成开发环境 - PyCharm 二、编写第一个Python程序2.1 在PyCharm中编写“Hello, World!”程序2.2 运行“Hello, World!”程序…

Windows 11+Visual Studio 2022 环境OpenCV+CUDA 12.5安装及踩坑笔记

周六日在家捣腾了一下&#xff0c;把过程记录下来。 前置条件 Visual Studio C 生成工具和本机显卡适配的CUDA与CUDA匹配的cuDNNPython 3NumPyOpenCV源代码以及对应版本的OpenCV-contrib模块源码CMake Visual Studio 下载Visual Studio&#xff08;我本机的是VS2022&#xf…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 智能驾驶(200分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线评测,专栏文章质量平均 93 分 最新华为OD机试目录: …

【Golang 面试基础题】每日 5 题(九)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

c/c++的内存管理(超详细)

一、c/c的内存分布 这是操作系统中对于内存的划分&#xff1a; 我们重点掌握以下几个区域即可&#xff1a; 1.栈 (调用函数会建立栈帧) 2.堆(动态开辟的空间) 3.数据段(静态区)&#xff1a;存放静态变量以及全局变量 4.代码段 (常量区) 先来看看一个题目&#xff1a; int…

蓝牙耳机百元之内怎么选?四款百元精品爆款蓝牙耳机盘点

在蓝牙耳机的海洋中&#xff0c;百元价位仿佛是一片神秘的绿洲&#xff0c;既诱人又充满未知&#xff0c;如何在众多选项中挑选出真正的精品呢&#xff1f;蓝牙耳机百元之内怎么选&#xff1f;这是许多消费者的共同疑问&#xff0c;带着这个疑问&#xff0c;作为蓝牙耳机发烧党…

【Python机器学习】朴素贝叶斯——条件概率

条件概率 假设现在有一个装了7块石头的罐子&#xff08;3块灰色&#xff0c;4块黑色&#xff09;&#xff0c;如果从中随机取出一块&#xff0c;灰色的可能性就是3/7&#xff0c;黑色的可能性是4/7。我们使用p(gray)来表示取到灰色石头的概率&#xff0c;其概率值可以通过灰色…

1.3 双向链表定义及部分实现

1.定义 单链表的问题&#xff1a;找后继容易&#xff0c; 找前驱难 双向链表&#xff08; Double Linked List &#xff09; &#xff1a;指的是构成链表的每个结点中设立两个指针域&#xff1a; 一个指向其直接前趋的指针域 prior &#xff0c;一个指向其直接后继的指针域 …

Tent混沌人工蜂群与粒子群混合算法遇到问题,具体问题及解决方案如文。

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

谷歌再被OpenAI截胡?训练数学AI拿下IMO银牌,不及SearchGPT放了空响..

昨夜谷歌振臂高呼&#xff1a;我们新的数学AI&#xff0c;能在IMO数学竞赛达到银牌水平&#xff01; 但就在谷歌发文的几个小时后&#xff0c;OpenAI就偷了谷歌的家&#xff1a; 发布新的搜索方式SearchGPT &#xff0c;剑指挑战谷歌的搜索引擎。 谷歌双模型并用攻克数学难题…

2. 卷积神经网络无法绕开的神——LeNet

卷积神经网络无法绕开的大神——LeNet 1. 基本架构2. LeNet 53. LeNet 5 代码 1. 基本架构 特征抽取模块可学习的分类器模块 2. LeNet 5 LeNet 5: 5 表示的是5个核心层&#xff0c;2个卷积层&#xff0c;3个全连接层.核心权重层&#xff1a;卷积层、全连接层、循环层&#xff…

LCD 横屏切换为竖屏-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

LCD 横屏切换为竖屏 横屏显示如何切换为竖屏显示 LCD 屏默认横屏显示 开发板配套的 LCD 屏默认都是横屏显示&#xff0c;如 4.3 寸、7 寸和 10.1 寸的不同分辨率的 RGB LCD 屏 固定坐标体系 &#xff08;以 800*480 分辨率为例&#xff09;横屏模式下的固定坐标&#xff1a;…

快醒醒,别睡了!...讲《数据分析pandas库》了—/—<5>

一、 1、修改替换变量值 本质上是如何直接指定单元格的问题&#xff0c;只要能准确定位单元地址&#xff0c;就能够做到准确替换。 1.1 对应数值的替换 具体用法如下&#xff1a; replace方法&#xff1a; df.replace(to_replace None :将被替换的原数值&#xff0c;所有…

【Python机器学习】决策树的构造——信息增益

决策树是最经常使用的数据挖掘算法。它之所以如此流行&#xff0c;一个很重要的原因就是不需要了解机器学习的知识&#xff0c;就能搞明白决策树是如何工作的。 决策树的优缺点&#xff1a; 优点&#xff1a;计算复杂度不高&#xff0c;输出结果易于理解&#xff0c;对中间值的…

linux集群架构--高可用--keepalived(13985字详解)

linux架构高可用 a.概述 高可用&#xff1a;HA HighAvailablity —>Keepalived生成VIP&#xff0c;DNS解析到这个IP地址即可 b.原理 keepalived 是基于VRRP协议实现高可用VRRP虚拟路由器冗余协议&#xff0c;最开始是给网络设备实现高可用&#xff0c;目前keepalive实现v…

项目实战1(30小时精通C++和外挂实战)

项目实战1&#xff08;30小时精通C和外挂实战&#xff09; 01-MFC1-图标02-MFC2-按钮、调试、打开网页05-MFC5-checkbox及按钮绑定对象06--文件格式、OD序列号08-暴力破解09-CE10-秒杀僵尸 01-MFC1-图标 这个外挂只针对植物大战僵尸游戏 开发这个外挂&#xff0c;首先要将界面…

GUL图形化界面操作(下部)

目录 ​编辑 前言 Swing 窗口 注意点 新增的组件 进度条组件 开关按钮 多面板和分割面板 多面板 分割面板 ​编辑 选项窗口 对话框带三个选项是&#xff0c;否&#xff0c;取消。 对话框提示输入文本: 前言 修炼中&#xff0c;该篇文章为俺很久前的学习笔记 Swi…

2024钉钉杯A题思路详解

文章目录 一、问题一1.1 问题1.2 模型1.3 目标1.4 思路1.4.1 样本探究1.4.2 数据集特性探究&#xff1a;1.4.3 数据预处理1.4.4 数据趋势可视化1.4.5 ARIMA和LSTM两种预测模型1.4.6 参数调整 二、问题二2.1 问题2.2 模型2.3 目标2.4 思路2.4.1 样本探究2.4.2 数据集特性探究2.4…

Radon(拉当) 变换:超详细讲解(附MATLAB,Python 代码)

Radon 变换 Radon 变换是数学上用于函数或图像的一种积分变换&#xff0c;广泛应用于图像处理领域&#xff0c;尤其是在计算机断层成像 (CT) 中。本文档将详细介绍 Radon 变换的数学含义及其在图像处理中的应用。 数学定义 Radon 变换的数学定义是将二维函数 f ( x , y ) f…