C++虚表结构详解

原文地址:https://www.learncpp.com/cpp-tutorial/125-the-virtual-table/

为了实现虚函数,C++使用动态绑定方式,称为虚表

虚表是一个包含函数的查找表,该查找表用于动态绑定方式解决函数调用问题,虚表有时候被称为“vtable”,"虚函数表",“虚方法表”,“分派表”。

虚表虽然用语言描述有点复杂,实际上非常简单。

首先,每个包含虚函数的类(或者继承自包含虚函数的类)会有自己的一个虚函数表。这个虚函数表是编译器在编译时创建的静态数组,虚函数表中有每一个虚函数对应的地址入口,每个类实例可以调用这些虚函数地址获取虚函数。该虚函数表中每个入口地址是一个简单的函数指针,该函数指针指向该类可以获取的最终派生子类的函数。

其次,编译器还在基类中添加了一个隐藏指针,我们称之为*__vptr,*__vptr在类实例创建时自动被设置,指向该类的虚函数表。和*this指针不同,*this实际上是一个编译器用于解析自引用的函数参数,*__vptr是一个真正的指针。因此,每个类实例会申请额外一个指针大小的空间给*__vptr,这意味着*__vptr会被子类继承,这很重要。

下面看一个简单例子:

class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base
{
public:
    virtual void function1() {};
};
 
class D2: public Base
{
public:
    virtual void function2() {};
};

上面有三个类,编译器会创建三个虚函数表,一个用于Base类,一个用于D1类,一个用于D2类

编译器还会给包含虚函数的最基类添加一个隐藏指针,这是编译器自动完成的,下面会介绍该隐藏指针在哪里被添加

class Base
{
public:
    FunctionPointer *__vptr;
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base
{
public:
    virtual void function1() {};
};
 
class D2: public Base
{
public:
    virtual void function2() {};
};

当一个类实例创建,*__vptr被设置指向该类的虚函数表。例如,当创建Base实例,*__vptr指向基类Base的虚函数表,当D1或D2的实例被创建时,*__vptr被设置分别指向D1和D2的虚函数表。

现在,讨论下这些虚函数表被填充的,因为这里只有两个虚函数,每个虚函数表有两个地址入口,一个指向function1(),一个指向function2()。请记住,当这些虚函数表被填充,每个地址入口被指向最外层子类的函数,当类实例调用时,调用该最外层子类函数。

Base对象的虚函数表很简单,Base对象只能访问Base成员。Base没有权限访问D1和D2中的函数。因此,function1的地址入口指向Base::function1(),function2的地址入口指向Base::function2()

D1的虚函数表稍微复杂,D1的实例可以访问D1和Base的成员。然而D1已经覆盖了function1(),D1::function1()是比Base::function1()更外层的子类,因此function1的地址入口指向D1::function1()。D1没有覆盖function2(),因此function2的地址入口指向Base::function2()。

D2的虚函数表和D1类似,function1的地址入口指向Base::function1(),function2的地址入口指向D2::function2()。

上面这个图非常直观,每个类的*__vptr指针指向该类的虚函数表,虚函数表中的地址入口指向该类对象可以调用到的最外层子类的函数。

因此考虑如下场景,当我们创建D1对象时,因为d1是D1对象,所以*__vptr被设置指向D1的虚函数表

int main()
{
    D1 d1;
}

现在我们设置一个基类指针指向D1,

int main()
{
    D1 d1;
    Base *dPtr = &d1;
}

注意,由于dPtr是基指针,它只指向了D1的基类部分,但是要注意到*__vptr在基类中,因此dPtr具有访问该指针的权限。最后注意,注意到dPtr->__vptr指向D1的虚函数表,因此即使dPtr是Base类型,它仍然具有访问D1虚函数表的权限(通过__vptr)

那当我们调用dPtr->function1()时发生什么呢?

int main()
{
    D1 d1;
    Base *dPtr = &d1;
    dPtr->function1();
}

首先,程序识别到function1()是一个虚函数。其次,程序使用dPtr->__vptr获取D1的虚函数表。最后,它查找D1的虚函数表中被调用的function1()版本。因此,dPtr->function1()被解析到D1::function1()

现在你或许会说,当dPtr真的指向Base对象而不是D1对象,它还会继续调用D1::function1()么?答案是NO

int main()
{
    Base b;
    Base *bPtr = &b;
    bPtr->function1();
}

在这种情况下,当b创建时,__vptr指向Base的虚函数表,而不是D1的虚函数表,因此bPtr->__vptr也会指向Base的虚函数表。Base的虚函数表中function1的地址入口指向Base::function1(),因此bPtr->function1()解析到Base::function1(),这是Base对象被调用时解析到的最外层子类的function1()版本。

通过使用这些虚函数表,编译器和程序可以保证程序调用解析到正确的虚函数,即使你只是使用基类的指针或引用。

调用虚函数会比调用非虚函数更慢,有以下原因:第一,我们使用*__vptr获取虚函数表,第二,我们必须索引虚函数表去找到正确的被调用函数,然后我们才能调用函数。因此,我们做了3次操作去找到被调用函数,普通调用只需要2次,或1次。然而,现代计算机中,这些增加的时间可以忽略不计。

另外,提醒以下,任何一个使用虚函数的类都有一个虚函数指针__vptr,每个类对象都会有一个额外的指针,虚函数功能很强大,但确实有性能成本!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值