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