> 文档中心 > 虚表简单分析

虚表简单分析

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

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

#include 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 FFcall 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 FFcall 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 FFcall 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 FFcall 000511F4  }

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

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

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

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

#include 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 FFcall 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 FFcall 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 FFcall 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的虚表的第一个函数地址不相同(因为进行了函数重写),而第二个函数的函数地址完全相同(因为我们并没有进行函数重写,所以都指向同一个位置)

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

为什么不配图?因为我懒