需要澄清C++虚拟呼叫的实施

Clarification Needed on C++ Virtual Call Implementation

本文关键字:呼叫 虚拟 C++      更新时间:2023-10-16

我对虚函数有一些疑问,或者更好的是我们可以说运行时多态性。据我说,我假设它的工作方式如下,

  1. 将为至少具有一个虚拟成员函数的每个类创建一个虚拟表 (V-Table(。我相信这是静态表,因此它是为每个类而不是为每个对象创建的。如果我在这里错了,请纠正我。

  2. 此 V 表具有虚函数的地址。如果类有 4 个虚函数,则此表有 4 个条目指向相应的 4 个函数。

  3. 编译器将添加一个虚拟指针 (V-Ptr( 作为类的隐藏成员。此虚拟指针将指向虚拟表中的起始地址。

假设我有这样的程序,

class Base
{
    virtual void F1();
    virtual void F2();
    virtual void F3();
    virtual void F4();
}
class Der1 : public Base  //Overrides only first 2 functions of Base class
{
    void F1(); //Overrides Base::F1()
    void F2(); //Overrides Base::F2()
}
class Der2 : public Base  //Overrides remaining functions of Base class
{
    void F3(); //Overrides Base::F3()
    void F4(); //Overrides Base::F4()
}
int main()
{
    Base* p1 = new Der1; //Believe Vtable will populated in compile time itself
    Base* p2 = new Der2;
    p1->F1(); //how does it call Der1::F1()
    p2->F3(); //how does it call Base::F3();
}

如果V表在编译时填充,为什么要将其称为运行时多态性?请使用上面的例子解释我有多少 vtables 和 vptr 以及它是如何工作的。据我说,3 个 Vtables 将用于 Base、Der1 和 Der2 类。在 Der1 Vtable 中,它有自己的 F1(( 和 F2(( 地址,而对于 F3(( 和 F4((,地址将指向基类。此外,3 Vptr 将作为隐藏成员添加到 Base、Der1 和 Der2 类中。如果一切都在编译时决定,那么在运行时究竟会发生什么?如果我的概念有误,请纠正我。

这显然是实现定义的,但大多数实现非常相似,或多或少与您描述的路线一致。

  1. 这是正确的。

  2. vtables 包含的不仅仅是指向函数的指针。通常有一个条目指向RTTI信息,并且通常有一些关于如何修复此指针的信息调用函数时(尽管这也可以使用蹦床(。 在虚拟基地的情况下,也可能有虚拟基的偏移量。

  3. 这也是正确的。 请注意,在施工期间和销毁,编译器将更改vptr作为动态对象的类型发生变化,并且在多个的情况下继承(有或没有虚拟基地(,会有更多比一个vptr. (vptr与关于类的基址,在多重继承,并非所有类都可以具有相同的基数地址。

至于你最后说的一句话:vtables在编译时填充时间,并且是静态的。 但是 vptr 是在运行时设置的,根据动态类型,函数调用使用它来找到 vtable 并调度呼叫。

在您的(非常简单的(示例中,有三个 vtable,一个用于每个班级。 因为只涉及简单的继承,所以有每个实例只有一个 VPTR,在 Base 和派生类。 Base的 vtable 将包含四个插槽,指向Base::f1Base::f2Base::f3Base::f4。用于Der1的 vtable 还将包含四个插槽,指向 Der1::f1Der1::f2Base::f3Base::f4。 视频桌因为Der2将指向Base::f1Base::f2Der2::f3Der2::f4 . Base的构造函数会将 vptr 设置为目录 Base ;派生类的构造函数将首先调用基类的构造函数,然后设置 VPTR到与其类型对应的 vtable。 (在实践中,在这样的简单的情况,编译器可能能够确定VPTR 从不用于构造函数中以Base,因此跳过设置它。 在更复杂的情况下,编译器看不到基类构造函数的所有行为,但是,情况并非如此。

至于为什么它被称为运行时多态性,请考虑一个函数:

void f(Base* p)
{
    p->f1();
}

实际调用的函数会有所不同,具体取决于无论p指向Der1还是Der2. 换句话说,它将在运行时确定。

C++标准没有指定如何实现虚函数调用,但这里有一个普遍接受的方法的简化示例。

从高级别的角度来看,v-table 如下所示:

基地

Index |  Function Address
------|------------------
    0 |  Base::F1
    1 |  Base::F2
    2 |  Base::F3
    3 |  Base::F4

Der1

Index |  Function Address
------|------------------
    0 |  Der1::F1
    1 |  Der1::F2
    2 |  Base::F3
    3 |  Base::F4

Der2

Index |  Function Address
------|------------------
    0 |  Base::F1
    1 |  Base::F2
    2 |  Der2::F3
    3 |  Der2::F4

当你创建p1p2时,它们会得到一个指针,分别指向Der1的 vtable 和 Der2 的 vtable。

p1->F1的调用基本上意味着"在p1的虚拟表上调用函数0"。 vptr[0]Der1::F1,所以它被称为。

之所以称为运行时多态性,是因为将为特定对象调用的函数是在运行时确定的(通过在对象的 vtable 中进行查找(。

它是实现定义的。在C++编程时,唯一应该关注的是,如果您声明方法virtual,指针或引用后面对象的运行时内容将决定将调用哪些代码。

也许你应该先阅读这个话题。这是C++具体的东西。

我不打算介绍四个虚函数和三个派生类型。可以这么说:对于最终基类,vtable 具有指向所有虚函数的基类版本的指针。对于派生类,vtable 具有指向派生类的所有虚函数的指针;当派生类重写基类函数时,该函数的函数指针指向该虚函数的派生类版本;当派生类继承虚函数时,函数指针指向继承的函数。