目录 Table of Contents
10 虚表
多态是如何实现的
这节主要研究 C++ 多态是如何实现的
代码如下 :
#include "stdafx.h"
#include <stdlib.h>
#include <windows.h>
class A
{
public:
int x;
virtual void Test()
{
printf("A\n");
}
};
class B:public A
{
public:
void Test()
{
printf("B \n")
}
}
void Fun(A* p)
{
p->Test(); // 多态
}
int main(int argc, char* argv[])
{
A a;
B b;
Fun(&b);
return 0;
}
如果去掉 virtual
关键字, 让父类 A 的 Test()
函数变成一个普通的函数, 观察反汇编代码, 你传的就算是子类 B 的对象进去, 调用的还是父类的函数
;p->Test(); // 多态
mov ecx, dword ptr [ebp+8]
call @ILT+30(A::Test) (00401023)
现在我们不去掉 virtual
, 让它继续是虚函数
这次穿进去父类 A 的对象, 看看反汇编代码
;p->Test(); // 多态
mov eax, dword ptr [ebp+8]
mov edx, dword ptr [eax]
mov esi, esp
mov ecx, dword ptr [ebp+8]
call dword ptr [edx] ; 这行代码
cmp esi, esp
call _chkesp (00401240)
注意标记的这行代码, 可以与上面的对比一下, 可以发现, 刚才调用的是 A 类的函数, 是写死的, 但是现在呢, 没有写死, 后面跟的不是一个具体的函数地址, 而是调用的这么一个格式, 我们在里面根本看不出来函数的地址到底是什么
因为函数的地址存放到了 [edx]
这个地址里面了, 如果 edx 为 1234 的话, 我函数的地址就是 1234 这个地址里面存放的那个值, 1234 存放的是 1, 那我要调用的函数地址就是 1.
/*如果你传的是父类的对象, 这个地址里面存的是父类的 Test()
地址; 如果你传的是子类的对象, 这个地址里面存的就是子类 Test()
的地址.*/
这种调用形式我们称为间接调用
所以这就解释了, 为什么加上虚函数之后就可以实现多态了呢 ? 因为编译器帮我们做了一些事情, 编译器说了, 只要你是虚函数, 当你再调用虚函数的时候, 我同意按照间接调用方式来给你生成汇编代码.
多态就是通过间接调用实现的.
我们现在弄清楚了什么是间接调用, 现在我们来看一看间接调用的时候这个地址是什么.
;p->Test(); // 多态
mov eax, dword ptr [ebp+8]
mov edx, dword ptr [eax]
mov esi, esp
mov ecx, dword ptr [ebp+8]
call dword ptr [edx] ; 这行代码
cmp esi, esp
call _chkesp (00401240)
我们一个个地分析一下
mov eax, dword ptr [ebp+8]
ebp+8 就是当前父类的指针, 作为参数传进来存到 eax 里面, eax 就相当于当前对象的首地址
mov edx, dword ptr [eax]
把对象的第一个成员取出来放到 edx 中
于是我们现在就疑惑了, 按照我们以前的理解的话, 这就是把第一个成员取出来, 然后调用了第一个成员的值所代表的地址那里的函数, 这就非常奇怪了
现在我们来看看, 写了虚函数之后, 这个对象的反汇编代码是怎样的的
int main(int argc, char* argv[])
{
A a;
B b;
//Fun(&b);
// 我们先简单输出一下 a 对象的大小
printf("%d \n", sizeof(a));
return 0;
}
按照我们以前的理解, 对象 a 应该是 4 个字节 (一个 int 型变量栈 4 个字节, 后面函数代码不占用当前对象的空间)
但是输出的结果却是 8, 说明 a 对象是 8 个字节, 多了 4 个字节
我们再做一个实验, 这次我们把虚函数给它去掉, 让它变成一个普通的函数, 我们再输出, 结果变回了 4 个字节
通过刚才的实验, 我们发现, 只要类里面有虚函数, 它就会在原来基础上多出 4 个字节
我们再加一个虚函数, 它还是只多出 4 个字节, 也就是说不管你有几个虚函数, 都是多出来 4 个字节
这 4 个字节是什么呢, 这 4 个字节就是我们要说的虚表
虚表
-
观察带有虚函数的对象大小 (virtual)
我们已经观察过了, 只要有虚函数就会多出 4 个字节
多出来的 4 个字节, 在对象第一个成员的前面
-
虚表的位置
这 4 个字节里面存的值是一个地址, 这个地址指向的是我们的虚表
虚表的地址存在当前对象的第一个位置
虚表的这个地址变量叫作虚指针
-
虚表的结构
你有几个虚函数, 我就往里面写几个函数的地址
-
虚表的内容
函数的地址
-
子类没有重写虚函数时的值
子类依然会创建虚指针, 只不过子类创建的虚指针指向的是父类的虚表
-
子类重写虚函数时的值
这就好说了, 父类的就是父类的虚表, 子类就是子类的虚表
-
;p->Test(); // 多态
mov eax, dword ptr [ebp+8]
mov edx, dword ptr [eax]
mov esi, esp
mov ecx, dword ptr [ebp+8]
call dword ptr [edx] ; 这行代码
cmp esi, esp
call _chkesp (00401240)
我们现在再来看一下这个汇编代码
首先 mov eax, dword ptr [ebp+8]
取参数是个对象, 存到 eax 里面去, eax 就是对象的首地址
然后 mov edx, dword ptr [eax]
取当前对象的第一个成员, 现在这里是虚表的地址
然后 call dword ptr [edx]
直接调用虚表的地址