虚表vtbl

普通函数的处理:一个特定的函数都会映射到特定的代码,无论时编译阶段还是连接阶段,编译器都能计算出这个函数的地址,调用即可。
虚函数的处理:被调用的函数不仅依据调用的特定函数,还依据调用的对象的种类。通常是由虚函数表(vtable)来实现的。


虚函数表的结构:它是一个函数指针表,每一个表项都指向一个函数。任何一个包含至少一个虚函数的类都会有这样一张表。需要注意的是vtable只包含虚函数的指针,没有函数体。实现上是一个函数指针的数组。虚函数表既有继承性又有多态性。每个派生类的vtable继承了它各个基类的vtable,如果基类vtable中包含某一项,则其派生类的vtable中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类vtable的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。
每一个类只有唯一的一个vtable,不是每个对象都有一个vtable,恰恰是每个同一个类的对象都有一个指针,这个指针指向该类的vtable(当然,前提是这个类包含虚函数)。那么,每个对象只额外增加了一个指针的大小,一般说来是4字节。


在类对象的内存布局中,首先是该类的vtable指针,然后才是对象数据。

分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了而是直接执行好了,怎么在二者之间作一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到真正的绑定。这个数据结构就是vtable,也就是编译期间建立vtable表,执行期间查表执行。

下面是通过基类的指针来调用虚函数时,所发生的一切:
step 1:开始执行调用 pA->run();(这里能判断到底是哪个对象)
step 2:取得对象的vtable的指针
step 3:从vtable那里获得函数入口的偏移量,即得到要调用的函数的指针
step 4:根据vtable的地址找到函数,并调用函数。
step 1和step 4对于一般函数是一样的,虚函数只是多了step 2和step 3。

对怎样调用对象derived的虚函数VFun1(),我们来看下面的这张图:

首先,取得对象中的虚表指针;然后,通过虚表指针找到相应的虚表;最后,通过在虚表内的偏移量找到相应的函数来调用。对象的前4个字节是用来存放虚表指针的,从第5个字节开始才是存放的自己的第一个成员变量。

虚表vtbl_第1张图片

解惑:
1基类和派生类是共用一表,还是各有各的表(物理上)
答:基类和派生类是各有各的表,也就是说他们的物理地址是分开的,基类和派生类的虚表的唯一关联是:当派生类没有实现基类虚函数的重载时,派生类会直接把自己表的该函数地址值写为基类的该函数地址值.

#include <iostream>

using namespace std;

class CDemoClass
{
private:
    int a;

public:
    CDemoClass(int a)
    {
        this->a = a;
    }

    virtual void func2()
    {
        cout << "func2" << endl;
    cout << "the a is: "<< this->a << endl;
    }
   
    virtual void func3()
    {
        cout << "func3" << endl;
    cout << "the a is: "<< this->a << endl;
    }

    virtual ~CDemoClass()
    {
        cout << "destructor" << endl;
    }

    virtual void func1()
    {
        cout << "func1" << endl;
    }
};

typedef void (*Func2)(CDemoClass*);
typedef void (*Func3)(CDemoClass*);

int main()
{
    CDemoClass* poMyObj = new CDemoClass(1);

    void* pVTbl = *((void**) poMyObj); // 取得对象的第一个项目,它存储的是虚表的地址【对象内部的第一个字段就是虚表指针】
    
    void* pVMethod = *((void**) pVTbl); // 取出虚表中的第一个条目,它存储的是第一个声明的虚函数的地址【虚表由若干个条目构成,每个条目都是一个指针,按声明顺序指向该类的各个虚函数】
    cout << pVMethod << endl;
//    __asm call pVMethod; // windows上
    Func2 f2 = (Func2) pVMethod;
    f2(poMyObj);

    pVMethod = *((void**) pVTbl + 1); // 第二个声明的虚函数
    cout << pVMethod << endl;
//    __asm call pVMethod; // windows上
    Func3 f3 = (Func3) pVMethod;
    f3(poMyObj);

    delete poMyObj;
    return 0;
}

TODO 例子可以更深一步,在父类和子类都有虚函数时,他们的虚表各是什么样的?


你可能感兴趣的:(虚表vtbl)