虚函数的反汇编

可以先看看这篇文章补充一下基础知识:c++虚函数表

虚函数机制

当类中有虚函数时,编译器会把类中所有虚函数的地址统一放在一张地址表中,这张表叫做虚函数地址表,位于程序的数据区。**同时编译器会在类中添加一个隐藏数据成员,称为虚表指针,该指针保存着虚函数表的首地址,用于记录和查找虚函数。**虚表指针的地址和this的值是一样的。

先来看一个简单的例子:

#include <stdio.h>

class Person
{
    public:
        virtual int getAge() { return age; }
        virtual void setAge(int age) { this->age = age; }
    private:
        int age;
};

int main(int argc, char const *argv[])
{
    Person person;

    return 0;
}

main函数的反汇编代码如下:

在这里插入图片描述

执行完构造函数之后,我们查看rcx中保存的地址所指向的内存地址:

在这里插入图片描述

我们反汇编00007ff7`ab3725c7和00007ff7`ab372b4e这两个地址:

在这里插入图片描述

可以发现,这就是两个虚函数的首地址,对象的地址就是虚函数表的首地址。

我们来看看默认构造函数里面做了什么:

在这里插入图片描述

默认构造函数把虚函数表的首地址赋值给了this指针(rcx),并返回之。

因为虚表信息在编译后会被链接到对应的执行文件中,所以获得的虚表地址是一个相对固定的地址。虚表中虚函数的地址排列顺序因虚函数在类中的声明顺序而定,先声明的虚函数的地址会被排列在虚表靠前的位置。第一个被声明的虚函数的地址在虚表的首地址处。

对于含有构造函数的类而言,其虚表初始化过程和默认构造函数相同,都是在对象首地址处保存虚表的首地址。

在析构函数中,对虚表指针进行的操作和构造函数是一样的。在析构函数中设置虚表指针,与构造函数中这样做,核心目的都是为了确保对象在其生命周期的不同阶段,能够通过正确的虚函数表来调用当前阶段所属类的虚函数,这是C++实现多态机制的基石。

析构是构造的逆过程,顺序是从派生类到基类。首先进入派生类的析构函数,此时对象仍然是一个完整的“派生类对象”,因此需要先执行派生类的析构函数体来清理派生类特有的资源。在派生类的析构函数体执行完毕后,编译器会自动插入代码,将虚表指针还原为基类的虚函数表,然后再调用基类的析构函数。这样,当执行流程进入基类的析构函数时,对象已经被“部分销毁”,其身份退回到了“基类对象”,虚表指针也必须与之匹配。

虚函数的调用

先看代码:

#include <stdio.h>

class Person
{
    public:
        virtual int getAge() { return age; }
        virtual void setAge(int age) { this->age = age; }
    private:
        int age;
};

int main(int argc, char const *argv[])
{
    Person person;
    person.getAge();

    return 0;
}

对于通过对象直接调用虚函数,是不需要查找虚函数表的,因为对象的类型在编译期就可以确定了,是静态绑定,没有多态的情况。

只有通过对象指针或者引用调用虚函数的时候,才需要查询虚表进行调用。

总结

this指针保存着对象的首地址,对象的首地址存储着虚表指针,虚表指针中存储着虚表的首地址。在调用虚函数的时候,需要通过this指针获取对象的首地址,然后访问这个地址上的虚表指针,通过虚表指针获取虚表的首地址,再通过虚表的首地址加上偏移量调用虚函数。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐