【C++】 为什么多继承子类重写的父类的虚函数地址不同?『 多态调用汇编剖析』

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

🌝每一个不曾起舞的日子,都是对生命的辜负


前言

本篇文章主要是为了解答有关多态的那篇文章那块的一个奇怪现象,大家还记得这张图片么?

你有没有发现:子类重写的func1函数地址竟然是不同的?

按常理讲:我们知道函数地址存储的是函数的指令的位置,这里『 应该是相同』的,才能保证对象在调用时都调用『 子类重写后的』func1方法 ,否则就失去了重写的意义了。

所以这里一定存在某些底层设计,那接下来就让我们转到『反汇编 』,来查看以下vs在这里是如何设计的吧。


欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟樊飞 (fanfei_c) - Gitee.com🌟

=========================================================================


1.构建模型

首先,为了方便研究,我们构建函数模型:

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1=1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2=2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1=3;
};int main()
{Derive d;// 多态调用Base1* p1 = &d;p1->func1();Base2* p2 = &d;p2->func1();return 0;
}

2.剖析

通过内存窗口我们得出这样的结构:

经过多态部分的学习,我们知道p1指针指向的对象内存中『 0x00819b94 』就是Base1的虚表指针,同样的p2指针指向的对象内存中『 0x00819ba8』就是Base2的虚表指针。

监视窗口也可以看出来这些:

注意:多态那篇文章我们已经提到过,vs的监视窗口这里有一个bug,就是没能显示出子类func3函数, 即子类d的虚函数表没有显示在监视窗口中,这里大家可以参考我多态部分的文章有详解,你也可以自己通过内存窗口验证。子类的虚函数表添加在继承的第一个父类的虚表后。

当然以上说的不是我们这篇文章的重点,只是一个回顾,更多详见『 樊梓慕』多态 - CSDN

我们主要研究func1函数的地址为什么是不同的? 

接下来我们转到反汇编:

2.1Base1类型的p1指针调用func1 

首先观察下p1调用func1的汇编代码,看看call到了哪里?

寄存器eax中存储的是『 0x00811230』,我们接着走:

很明显是一个jmp指令,再继续:

到这,我们就成功跳转到了子类重写的func1函数。

这也是正常情况下函数调用的过程。

那接下来我们来研究p2指针调用func1又是怎样的过程呢?

2.2Base2类型的p2指针调用func1

同样的eax中存储的地址是什么呢?继续往下走:

 同样是一个jmp指令,再继续:

这里jmp到了给ecx减8,然后再jmp,在ecx减8之前,我们先来看看ecx中存储的是什么:

注意:成员函数的调用中,寄存器ecx通常用来存储this指针

那减去8之后,很明显就变成了『 0x00fcfba0』,这个地址是什么呢?

其实就相当于子类Derive的this指针。

到这其实已经非常明显了,那我们继续看jmp到了哪里:

到这里,你有没有发现这步的jmp和Base1类型的p1指针调用func1的jmp已经完全一样了,继续:

好,成功调用到func1函数。


3.总结

总结一下,Base2类型的p2指针调用func1函数时多做了一些工作,多jmp了一步,jmp的这一步目的是为了调整this指针,让this指针指向子类的头部。

为什么呢?

  • 因为p1和p2都是父类指针指向子类对象,p1是因为巧合恰好与子类头部位置重合,所以this指针位置本就是正确的,不需要额外操作。
  • 而p2的this指针指向的位置是子类中自己的虚表位置,所以需要额外jmp一步,使p2指针指向的子类对象的this指针进行一定的偏移,让this指针到达正确的位置,才能完成调用func1的操作。

😈剖析底层,修炼内功😈


=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================
 

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

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

相关文章

《剑指offer》

本专题是分享剑指offer的一些题目&#xff0c;开始刷题计划。 二维数组的中的查找【https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId13&tqId11154&ru/exam/oj】 描述 在一个二维数组array中&#xff08;每个一维数组的长度相同&#xff0…

关于Django的中间件使用说明。

目录 1.中间件2. 为什么要中间件&#xff1f;3. 具体使用中间件3.1 中间件所在的位置&#xff1a;在django的settings.py里面的MIDDLEWARE。3.2 中间件的创建3.3 中间件的使用 4. 展示成果 1.中间件 中间件的大概解释&#xff1a;在浏览器在请求服务器的时候&#xff0c;首先要…

【JAVA-Day88】Java字符串和JSON对象的转换

Java字符串和JSON对象的转换 Java字符串和JSON对象的转换摘要引言一、什么是JSON二、JSON的应用场景三、JSON对象转字符串3.1 使用 Jackson 库实现 JSON 对象转字符串3.2 使用 Gson 库实现 JSON 对象转字符串 四、JSON字符串转对象4.1 使用 Jackson 库实现 JSON 字符串转对象4.…

Editing While Playing 使用 Easyx 开发的 RPG 地图编辑器 tilemap eaitor

AWSD移动画布 鼠标右键长按拖拽 鼠标左键长按绘制 可以边拖拽边移动画布边绘制。 F1 导出 DLC F2 导入DLC author: 民用级脑的研发记录 1309602336qq.com 开发环境&#xff1a; 内置 easyx 的 devc 5.11 或者 VS 2022 TDM GCC 4.9.2 64-bit c11及以上都可运行 windows 环境运行…

通过深度学习和人脸图像进行年龄段估计matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1深度学习网络 4.2 人脸特征提取 4.3 回归模型构建 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022a 3.部分核心程序 ..................................…

平时积累的FPGA知识点(8)

平时在FPGA群聊等积累的FPGA知识点&#xff0c;第八期&#xff1a; 21 FFT IP核 有遇到过FFT IP核测量频率不准确的问题吗&#xff1f;大部分情况下都是准的&#xff0c;偶尔偏差比较大&#xff0c;IP核输入的数据用matlab计算出的频率是对的。 解释&#xff1a;可能是采样点…

docker (三)-开箱即用常用命令

一 docker架构 拉取镜像仓库中的镜像到本地&#xff0c;镜像运行产生一个容器 registry 镜像仓库 registry可以理解为镜像仓库&#xff0c;用于保存docker image。 Docker Hub 是docker官方的镜像仓库&#xff0c;docker命令默认从docker hub中拉取镜像。我们也可以搭建自己…

数据结构——6.1 图的基本概念

第六章 图 6.1 图的基本概念 概念 图的概念&#xff1a;G由点集V和边集E构成&#xff0c;记为G(V,E)&#xff0c;边集可以为空&#xff0c;但是点集不能为空 注意&#xff1a;线性表可以是空表&#xff0c;树可以是空树&#xff0c;但图不可以是空&#xff0c;即V一定是非空集…

Pr教程1-8节笔记

第一课 认识PR以及PR的学习方法 学习任务&#xff1a; 1、熟练掌握PR软件&#xff0c;同时掌握剪辑技术以及常用于制作特效的效果器。 2、认识PR软件的名称、主要功能以及用途作用。 3、明白学习PR我们能做些什么以及PR的学习方法。 知识内容&#xff1a; 1、PR是专门用于视…

Linux 基础/子目录分配/文件路径

在Linux系统中&#xff0c;整个系统只具有一个根目录“/”&#xff0c;用斜杠表示。根目录是整个文件系统的顶层目录&#xff0c;在他下面可以创建其他的目录和文件。 Linux中的子目录分配&#xff1a; /bin - 基本命令的二进制文件&#xff0c;这些命令可供所有用户使用&am…

(10)Hive的相关概念——文件格式和数据压缩

目录 一、文件格式 1.1 列式存储和行式存储 1.1.1 行存储的特点 1.1.2 列存储的特点 1.2 TextFile 1.3 SequenceFile 1.4 Parquet 1.5 ORC 二、数据压缩 2.1 数据压缩-概述 2.1.1 压缩的优点 2.1.2 压缩的缺点 2.2 Hive中压缩配置 2.2.1 开启Map输出阶段压缩&…

详解 Redis 实现数据去重

✨✨ 欢迎大家来到喔的嘛呀的博客✨✨ &#x1f388;&#x1f388;希望这篇博客对大家能有帮助&#x1f388;&#x1f388; 目录 言 一. Redis去重原理 1. Redis Set 数据结构 2. 基于 Set 实现数据去重 3. 代码示例 4. 总结 …