10_虚表

目录 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 个字节就是我们要说的虚表

虚表

  1. 观察带有虚函数的对象大小 (virtual)

    我们已经观察过了, 只要有虚函数就会多出 4 个字节

    多出来的 4 个字节, 在对象第一个成员的前面

  2. 虚表的位置

    这 4 个字节里面存的值是一个地址, 这个地址指向的是我们的虚表

    虚表的地址存在当前对象的第一个位置

    虚表的这个地址变量叫作虚指针

  3. 虚表的结构

    你有几个虚函数, 我就往里面写几个函数的地址

  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] 直接调用虚表的地址