虚表简单分析

  • 虚表
    • 在C++中要实现多态,需要借助虚函数 用virtual关键词修饰

先看下面的代码,可以实现多态吗??

#include <iostream>

class Fu{
private:
    int a;
public:
    void Test() {
        printf("Fu-->Test()\n");
    }
};
class Zi :public Fu {
public:
    void Test() {
        printf("Zi-->Test()\n");
    }
};



int main()
{
    Fu* p;
    Fu fu;
    Zi zi;

    p = &fu;
    p->Test();

    p = &zi;
    p->Test();
}

运行结果是什么??
跑一下就知道了,不想贴截图,自己跑吧

结果是

Fu–>Test()

Fu–>Test()

并不是

Fu–>Test()

Zi–>Test()

为什么没有达到我们想要的效果?为什么没有达到多态的效果?

我们也进行方法重写了,为什么执行的还是父类的Test函数呢?

先看下汇编代码

    p = &fu;
00051B02 8D 45 E8             lea         eax,[fu]  
00051B05 89 45 F4             mov         dword ptr [p],eax  
    p->Test();
00051B08 8B 4D F4             mov         ecx,dword ptr [p]  
00051B0B E8 E4 F6 FF FF       call        Fu::Test (0511F4h)  

    p = &zi;
00051B10 8D 45 DC             lea         eax,[zi]  
00051B13 89 45 F4             mov         dword ptr [p],eax  
    p->Test();
00051B16 8B 4D F4             mov         ecx,dword ptr [p]  
00051B19 E8 D6 F6 FF FF       call        Fu::Test (0511F4h)

(可能有人看到的是这样的)

    p = &fu;
00051B02 8D 45 E8             lea         eax,[ebp-18h]  
00051B05 89 45 F4             mov         dword ptr [ebp-0Ch],eax  
    p->Test();
00051B08 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
00051B0B E8 E4 F6 FF FF       call        000511F4  

    p = &zi;
00051B10 8D 45 DC             lea         eax,[ebp-24h]  
00051B13 89 45 F4             mov         dword ptr [ebp-0Ch],eax  
    p->Test();
00051B16 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
00051B19 E8 D6 F6 FF FF       call        000511F4  
}

visual studio有个显示符号名的选项
,其实看到哪种一样

我们把这块汇编代码分成四块来看,第一块和第三块毫无疑问,没什么问题

第二段和第四段汇编代码,简直一模一样,全部都call向了同一个地址0511F4h,按理说应该指向不同的地址才对,因为我们进行了函数重写,所以说编译器并不知道我们进行了重写才都指向了同一个地址

那么我们需要让编译知道我们进行了重写,应该指向不同的函数,那么就用到了C++中的virtual 关键字

#include <iostream>

class Fu{
private:
    int a;
public:
    virtual void Test() {
        printf("Fu-->Test()\n");
    }
};
class Zi :public Fu {
public:
    void Test() {
        printf("Zi-->Test()\n");
    }
    
};



int main()
{
    Fu* p;
    Fu fu;
    Zi zi;

    p = &fu;
    p->Test();

    p = &zi;
    p->Test();
    
}



下面在运行试试,我们得到了想要的结果

Fu–>Test()

Zi–>Test()

    p = &fu;
00B11B52 8D 45 E4             lea         eax,[ebp-1Ch]  
00B11B55 89 45 F4             mov         dword ptr [ebp-0Ch],eax  
    p->Test();
00B11B58 8B 45 F4             mov         eax,dword ptr [ebp-0Ch]  
00B11B5B 8B 10                mov         edx,dword ptr [eax]  
00B11B5D 8B F4                mov         esi,esp  
00B11B5F 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
00B11B62 8B 02                mov         eax,dword ptr [edx]  
00B11B64 FF D0                call        eax  
00B11B66 3B F4                cmp         esi,esp  
00B11B68 E8 F5 F6 FF FF       call        00B11262  

    p = &zi;
00B11B6D 8D 45 D4             lea         eax,[ebp-2Ch]  
00B11B70 89 45 F4             mov         dword ptr [ebp-0Ch],eax  
    p->Test();
00B11B73 8B 45 F4             mov         eax,dword ptr [ebp-0Ch]  
00B11B76 8B 10                mov         edx,dword ptr [eax]  
00B11B78 8B F4                mov         esi,esp  
00B11B7A 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
00B11B7D 8B 02                mov         eax,dword ptr [edx]  
00B11B7F FF D0                call        eax  
00B11B81 3B F4                cmp         esi,esp  
00B11B83 E8 DA F6 FF FF       call        00B11262  
    

我们看第二段和第四段,我们发现都call了eax

00B11B7D 8B 02 mov eax,dword ptr [edx]
00B11B7F FF D0 call eax

eax的值跟[ebp-0Ch]有关,也就是p里面存储的值,第一个p存了fu的地址,第二个p存了zi的地址,两个p个存了不同的值,最终导致eax的值不同,从而实现了指向不同的函数地址

总结一下

多态调用函数是通过间接调用的,并非直接call向固定值

我们仔细看一下第二段和第四段,我们可以看到一个奇怪的地方,他在调用函数前多取了一次地址,这就意味着fu或者zi中多了一个4字节的指针

简单验证一下

    printf("%d\n", sizeof(fu));
    printf("%d\n", sizeof(zi));

如果没有多出4字节的话,大小应该为4

我们跑一下,发现大小为8,确实多出了4个字节,那么这4个字节是什么?

我们将fu和zi添加监视,我们发现多出来一个 _vfptr 叫做虚表的东西

顾名思义就是一个表嘛

看看里面存的什么东西,啊哈,是个函数地址。

既然是一个表,那么肯定可以存储很多个函数地址咯

试试

class Fu{
private:
    int a;
public:
    virtual void Test() {
        printf("Fu-->Test()\n");
    }
    virtual void Test1() {
        printf("Fu-->Test1()\n");
    }

};
class Zi :public Fu {
public:
    void Test() {
        printf("Zi-->Test()\n");
    }
    void Test1() {
        printf("Zi-->Test1()\n");
    }
    
};

猜想一下,fu和zi占8个字节,虚表里存了两个函数地址

添加监视,运行一下,发现跟我们猜想的一样

当我们调用Test2()时,看下汇编代码

欸嘿

    p = &fu;
00435690 8D 45 E4             lea         eax,[ebp-1Ch]  
00435693 89 45 F4             mov         dword ptr [ebp-0Ch],eax  
    //p->Test();
    p->Test1();
00435696 8B 45 F4             mov         eax,dword ptr [ebp-0Ch]  
00435699 8B 10                mov         edx,dword ptr [eax]  
0043569B 8B F4                mov         esi,esp  
0043569D 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
004356A0 8B 42 04             mov         eax,dword ptr [edx+4]  
004356A3 FF D0                call        eax  
004356A5 3B F4                cmp         esi,esp  
004356A7 E8 B6 BB FF FF       call        00431262  
    p = &zi;
004356AC 8D 45 D4             lea         eax,[ebp-2Ch]  
004356AF 89 45 F4             mov         dword ptr [ebp-0Ch],eax  

我们可以得出:

利用virtual修饰的函数,会存储在虚表中,进行函数调用通过虚表进行间接调用

我们观察虚表,发现虚表内存储的地址不相同,这是因为我们进行函数重写过,如果不进行重写呢?

我们把子类的test1函数删掉,在观察一下虚表,fu和zi的虚表的第一个函数地址不相同(因为进行了函数重写),而第二个函数的函数地址完全相同(因为我们并没有进行函数重写,所以都指向同一个位置)

本文没有进行相关配图,建议自己动手实践一下,以便理解

为什么不配图?因为我懒

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值