虚表剖析

2 篇文章 0 订阅

首先介绍一下虚表:在C++语言中,每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚拟函数表(简称:虚表),表中的每一个元素都指向一个虚函数的地址。(注意:虚表是从属于类的)
注:本文中所有程序的运行环境是vs2013

一.虚表的存在形式

class Base
{
public:
    virtual void fun1()
    {
        cout << "Base::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Base::fun2()" << endl;
    }
}

当有如下一个类Base,sizeof(Base)的值是多少?没有学习虚表之 前,我肯定会回答是0。而现在我知道了它的大小是4,这是为什么呢?
我们来看内存窗口:

这里写图片描述
这是Base 对象空间里的内容。
显然70 cc 0b 00是一个地址,那么我们转到这个地址去看看,那里都有些什么东西
这里写图片描述
这里面又是两个地址,让我们大胆地猜测一下:这两个地址就是类对象空间中两个虚函数的地址。接下来我们用一段代码来验证上面的猜测

class Base
{
public:
    virtual void fun1()
    {
        cout << "Base::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Base::fun2()" << endl;
    }
privateint a;
    int b;
};
typedef void(*fun)();

void funtest(Base&b)//Base类引用作为形参
{
    int i = 0;
    /*把b对象空间的前面4个字节的地址取出来,转化为int*类型*/
    int*p = (int*)*(int*)(&b);
    fun fn = (fun)*p;//把该地址转化为void*类型,便于函数的调用(验证上面的假设)
    while (fn)//直到遇上末尾的00 00 00 00未知,调用所有虚函数
    {
        fn();
        p++;
        fn = (fun)*p;
    }
}

void test()
{
    Base b;
    funtest(b);
}

执行程序后打印这里写图片描述

显然类对象空间首地址指向的空间里的地址就是虚函数的地址。即编译器会为包含虚函数的类加上一个成员变量,是一个指向该虚函数表的指针(常被称为vptr),每一个由此类别派生出来的类,都有这么一个vptr。在这里我们可以用一张图更明了地表示出这种现象
这里写图片描述

二.虚表的生成
虚表在编译完之后就生成了,这里我们要说的是派生类中虚表是如何生成的:

在这里编译器会为两个类合成缺省构造函数,那么这两个构造函数都做了一些什么事情呢?
(1)基类Base的构造函数把虚表地址放在对象空间的头四个字节
(2)派生类Derived先调用基类的构造函数,看一下派生类是否有定义虚函数,如果有就将头四个字节的内容改为Derived的虚表地址。派生类的虚表内容按基类虚表内容的顺序来,派生类的虚表先拷贝一份基类的虚标,如果派生类中重新定义了某个虚函数,就在虚标的该位置上改动,最后检测是否有定义新的虚函数,如果有,就加在虚表的最后。
这里写图片描述

举个例子来验证一下:


class Base
{
public:
    virtual void fun1()
    {cout << "Base::fun1()" << endl;}
    virtual void fun2()
    {cout << "Base::fun2()" << endl;}
    virtual void fun3()
    {cout << "Base::fun3()" << endl;}
private:
    int base_a;
    int base_b;
};

class Derived :public Base
{
    virtual void fun3()//缺少fun1()
    {cout << "Derived::fun3()" << endl;}
    virtual void fun2()//fun2()和fun3()逆序
    {cout << "Derived::fun2()" << endl;}
    virtual void fun4()//fun4()是新定义的
    {cout << "Derived::fun4()" << endl;}
private:
    int derived_a;
    int derived_b;
};

typedef void(*fun)();

void funtest(Base&b)
{
    int i = 0;
    int*p = (int*)*(int*)(&b);
    fun fn = (fun)*p;
    while (fn)
    {
        fn();
        p++;
        fn = (fun)*p;
    }
}

void test()
{
    Base b;
    Derived p;
    cout << "Base vptf\n";
    funtest(b);
    cout << "Derived vptf\n";
    funtest(p);
}

输出:

这里写图片描述

三.虚表的调用过程
基于上面的代码,给出一段调用代码:

void test(Base&b)
{
    b.fun1();
    b.fun2();
    b.fun3();
}

int main()
{
    Base b;
    Derived p;
    test(b);
    test(p);
    return 0;
}

转到反汇编:

    b.fun1();
0100611E  mov     eax,dword ptr [b] //传this指针 
01006121  mov     edx,dword ptr [eax]//拿到对象前四字节上的内容  
01006123  mov     esi,esp  
01006125  mov     ecx,dword ptr [b]  
01006128  mov     eax,dword ptr [edx]//拿到虚表首地址,即fun1()地址
0100612A  call    eax  //调用该虚函数
0100612C  cmp     esi,esp  
0100612E  call    __RTC_CheckEsp (01001343h)  
    b.fun2();
01006133  mov     eax,dword ptr [b]  //传this指针  
01006136  mov     edx,dword ptr [eax]//拿到对象前四字节上的内容    
01006138  mov     esi,esp  
0100613A  mov     ecx,dword ptr [b]  
0100613D  mov     eax,dword ptr [edx+4]//fun1()地址加上偏移即fun2()地址  
01006140  call    eax //调用该函数

综上虚函数的调用过程为:
1.传入this指针
2. >>取虚表地址>>虚表地址加上偏移拿到该函数的地址>>调用函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值