C++之虚函数

在C++中可以通过继承基类的虚函数来实现多态,virtual就是虚函数的关键字,当编译器扫描一个类定义时,就可将该类所有标记为virtual的函数找出,然后分配一个全局数组保存他们的地址,这时就可以给保存函数地址的数组一个新称号“虚表”,就是虚函数的地址表。而且虚函数第一次声明的顺序就决定了他们在虚表中存储的顺序。

当生成虚表后,编译器在编译一个有虚函数的类(包括自己没有,但是基类有)的构造函数时,就可在函数的一开始加入代码,将虚表的地址赋给对象的前4个字节(其实就是在对象的前4个字节保存指向虚表的指针)。

class A {
public:
    virtual void p1();
    virtual void p2();
    virtual void p3();
};

class B : public A {
public:
    virtual void p2();
};

这里首先定义了一个类A,其含有3个虚函数,然后定义一个类B继承类A,他们的对象模型布局如下图。
img

在实例化的每一个对象中,对象中的首4个字节保存的都是指向虚表的地址。接下来就来验证该对象模型,IDE是Visual Studio 2019。

#include <iostream>
class A {
public:
    virtual void work() { std::cout << "I am work" << std::endl; };
    virtual void finish() { std::cout << "I am finish" << std::endl; };
};

int main()
{
    A* aPtr;
    aPtr = new A;
    aPtr->work();
    aPtr->finish();
}

在aPtr -> work()处打断点,开始调试,触发断点,调出反汇编窗口。

   aPtr->work();
007428A7 8B 45 F8             mov         eax,dword ptr [aPtr]
    aPtr->work();
007428AA 8B 10                mov         edx,dword ptr [eax]
007428AC 8B F4                mov         esi,esp
007428AE 8B 4D F8             mov         ecx,dword ptr [aPtr]
007428B1 8B 02                mov         eax,dword ptr [edx]
007428B3 FF D0                call        eax
007428B5 3B F4                cmp         esi,esp
007428B7 E8 2D EA FF FF       call        __RTC_CheckEsp (07412E9h)
    aPtr->finish();
007428BC 8B 45 F8             mov         eax,dword ptr [aPtr]
007428BF 8B 10                mov         edx,dword ptr [eax]
007428C1 8B F4                mov         esi,esp
007428C3 8B 4D F8             mov         ecx,dword ptr [aPtr]
007428C6 8B 42 04             mov         eax,dword ptr [edx+4]
007428C9 FF D0                call        eax
007428CB 3B F4                cmp         esi,esp
007428CD E8 17 EA FF FF       call        __RTC_CheckEsp (07412E9h)
  • 在调用work()函数之前,将对象的aPtr的地址传入eax中,再将对象的首4个字节内容赋值给edx,此时edx保存的就是指向虚表的指针;
  • 然后将aPtr的地址传递给ecx,这一步就是在传递this指针;
  • 最后将edx的内容赋值给eax,此时eax保存的就是第一个虚函数work()的地址,紧接着调用该函数。

类似可以分析出finish()函数的调用过程,在调用finish()过程中:eax,dword ptr [edx+4],将edx+4赋值给eax,edx+4就是虚表中第二个函数(finish())的地址。这里从反汇编的角度分析了虚函数的调用过程,接下来不采用对象来调用虚函数,而是直接利用虚表来调用虚函数。

代码如下:

#include <iostream>
class VirClass {
public:
    virtual void p1() { std::cout << "hello p1" << std::endl; };
    virtual void p2() { std::cout << "hello p2" << std::endl; }
};

int main()
{
    VirClass vc;
    void** aPtr;
    void** ptrVFT;           //指向虚表指针
    void* pVirFunc;          //指向虚函数
    aPtr = (void**)&vc;      //将vc的地址赋值给aPtr
    ptrVFT = (void**)*aPtr;  //将vc对象中指向虚表的指针赋值给ptrVFT
    pVirFunc = *ptrVFT;      //虚表指针指向的第一个函数是p1(),将p1()地址赋值给pVirFunc
    _asm {
        mov ecx, aPtr;       //传递this指针
        call pVirFunc;       //调用p1()
    };

    pVirFunc = *(ptrVFT + 1); //此时pVirFunc指向p2()地址

    _asm {
        mov ecx, aPtr;       //传递this指针
        call pVirFunc;       //调用p2()
    };
    return 0;
}

定义一个对象,先找出其指向虚表的指针,再根据该指针找出虚函数的地址,传递this指针,调用虚函数,运行结果如下,调用成功。
img
本文简要分析虚函数实现机制,从底层剖析了虚函数的调用,在这里或许你该明白了为什么“virtual”不能与“static”一起使用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值