C++ 多态(二)虚函数表

C++ 多态(二)虚函数表

The virtual table


概述

为了实现虚函数,C++ 使用一种特殊的后期绑定形式,称为虚表。虚表是函数的查找表,用于以动态/后期绑定的方式解析函数调用。虚表有时有其他名称,如“虚表”、“虚函数表”、“虚方法表”或“调度表”。

因为使用虚函数并不需要了解虚表的工作方式,所以可以将此部分视为可选阅读。

虚表实际上非常简单,尽管用文字来描述有点复杂。首先,每个使用虚函数的类(或派生自使用虚函数的类)都有自己的虚表。这个表是编译器在编译时设置的一个静态数组。虚表包含一个条目,每个虚函数都可以被类的对象调用。这个表中的每个条目只是一个函数指针,指向该类可访问的最深派生的函数。

其次,编译器还添加了一个指向基类的隐藏指针,我们将其称为 *__vptr。*__vptr 在创建类实例时(自动)设置,使其指向该类的虚表。*this 指针实际上是编译器用来解析自引用的函数形参,与此不同,*__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() {};
};

因为这里有3个类,所以编译器将设置3个虚表:一个用于 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 类型的对象时会发生什么:

int main()
{
    D1 d1;
}

因为 d1 是一个 d1 对象,所以 d1 的 *__vptr 被设置为 d1 虚表。

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

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

注意,因为 dPtr 是一个 Base 指针,所以它只指向 d1 的 Base 部分。但是,也要注意,*__vptr 是在类的 Base 部分,所以 dPtr 可以访问这个指针。最后,请注意 dPtr->__vptr 指向 D1 虚表! 因此,即使 dPtr 是 Base 类型,它仍然可以访问 D1 的虚表(通过 __vptr)。

那么,当我们尝试调用 dPtr->function1() 时会发生什么?

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

首先,程序识别出 function1() 是一个虚函数。第二,程序使用 dPtr->__vptr 来访问 D1 的虚表。第三,它查找在 D1 的虚表中调用 function1() 的哪个版本。这个值被设置为 D1::function1()。因此,dPtr->function1() 解析为 D1::function1()!

现在,您可能会说,“但是如果 dPtr 真的指向 Base 对象而不是 D1 对象呢?” 它还会调用 D1::function1() 吗?答案是否定的。

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

在这种情况下,当 b 被创建时,__vptr 指向 Base 的虚表,而不是 D1 的虚表。因此,bPtr->__vptr 也将指向 Base 的虚表。function1() 的 Base 的虚表项指向 Base::function1()。因此,bPtr->function1() 解析为 Base::function1(),这是一个 Base 对象应该能够调用的 function1() 的最深派生版本。

通过使用这些表,编译器和程序能够确保函数调用解析为适当的虚函数,即使您只使用基类的指针或引用!

调用虚函数比调用非虚函数慢有以下几个原因:首先,我们必须使用*__vptr 来访问适当的虚表。其次,我们必须对虚表建立索引,以找到要调用的正确函数。只有这样我们才能调用这个函数。因此,我们必须执行 3 个操作来找到要调用的函数,而不是普通的间接函数调用需要 2 个操作,或者直接函数调用需要 1 个操作。然而,对于现代计算机来说,这些额外的时间通常是微不足道的。

还要提醒一下,任何使用虚函数的类都有一个*__vptr,因此该类的每个对象都要大一个指针。虚拟函数非常强大,但它们确实有性能成本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏068

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值