虚表简单分析
- 虚表
- 在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的虚表的第一个函数地址不相同(因为进行了函数重写),而第二个函数的函数地址完全相同(因为我们并没有进行函数重写,所以都指向同一个位置)
本文没有进行相关配图,建议自己动手实践一下,以便理解
为什么不配图?因为我懒