👀樊梓慕:个人主页
🎥个人专栏:《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的操作。
😈剖析底层,修炼内功😈
=========================================================================
如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容
🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎
🌟~ 点赞收藏+关注 ~🌟
=========================================================================