2

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

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

  2. 这个 V-Table 有虚函数的地址。如果类有 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-Table 在编译时填充,为什么将其称为运行时多态性?请使用上面的示例向我解释有多少 vtable 和 vptr 以及它是如何工作的。根据我的说法,Base、Der1 和 Der2 类将有 3 个 Vtable。在 Der1 Vtable 中,它有自己的 F1() 和 F2() 地址,而对于 F3() 和 F4(),地址将指向 Base 类。此外 3 Vptr 将作为隐藏成员添加到 Base、Der1 和 Der2 类中。如果一切都是在编译时决定的,那么在运行时究竟会发生什么?如果我的概念有误,请纠正我。

4

4 回答 4

5

它显然是实现定义的,但大多数实现都非常相似,或多或少地沿着您描述的路线。

  1. 这是对的。

  2. vtables 不仅仅包含指向函数的指针。通常有一个指向 RTTI 信息的入口,通常还有一些关于如何在调用函数时修复 this 指针的信息(尽管这也可以使用蹦床来完成)。在虚拟基地的情况下,虚拟基地也可能存在偏移。

  3. 这也是正确的。请注意,在构造和销毁过程中,编译器会随着vptr对象的动态类型的变化而改变 ,并且在多重继承(有或没有虚基)的情况下,会有多个vptr. (vptr相对于类的基地址有一个固定的偏移量,并且在多重继承的情况下,并非所有类都可以具有相同的基地址。)

至于您的最后评论:vtables 在编译时填充,并且是静态的。但是 vptr 是在运行时根据动态类型设置的,函数调用使用它来查找 vtable 并调度调用。

在您的(非常简单的)示例中,有三个 vtable,每个类一个。因为只涉及简单的继承,所以每个实例只有一个 vptr,Base在派生类之间共享。的 vtable将包含四个插槽,Base指向Base::f1Base::f2和。vtable for也将包含四个插槽,指向 、和。的 vtable将指向、和。for 的构造函数会将 vptr 设置为Base::f3Base::f4Der1Der1::f1Der1::f2Base::f3Base::f4Der2Base::f1Base::f2Der2::f3Der2::f4BaseBase; 派生类的构造函数将首先调用基类的构造函数,然后将 vptr 设置为与其类型对应的 vtable。(实际上,在这种简单的情况下,编译器可能能够确定 vptr 从未在构造函数中使用 to Base,因此跳过设置它。在更复杂的情况下,编译器无法看到基础的所有行为但是,类构造函数并非如此。)

至于为什么叫运行时多态,考虑一个函数:

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

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

于 2013-02-08T15:17:32.623 回答
4

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

从高级的角度来看,v-tables 看起来像这样:

基地

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

德尔1

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

德尔2

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

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

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

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

于 2013-02-08T15:16:28.290 回答
2

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

也许您应该先阅读有关该主题的内容是 C++ 特定的东西。

于 2013-02-08T14:50:29.783 回答
0

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

于 2013-02-08T15:20:02.793 回答