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

时间:2013-02-08 14:47:36

标签: c++ oop

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

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

  2. 此V-Table具有虚拟功能的地址。如果该类有4个虚函数,那么该表有4个条目指向相应的4个函数。

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

  4. 假设我有这样的程序,

    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在编译时填充,为什么称它为Run Time Polymorphism?请使用上面的例子解释我有多少vtable和vptr以及它是如何工作的。据我说,3个Vtables将用于Base,Der1和Der2类。在Der1 Vtable中,它具有自己的F1()和F2()地址,而对于F3()和F4(),地址将指向Base类。此外,还将在Base,Der1和Der2类中添加3个Vptr作为隐藏成员。如果在编译时确定所有内容,那么在运行时会发生什么?如果我在概念上错了,请纠正我。

4 个答案:

答案 0 :(得分:5)

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

  1. 这是正确的。

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

  3. 这也是正确的。注意在施工期间 销毁时,编译器会将vptr更改为动态 对象的类型更改,并且在多个的情况下更改 继承(有或没有虚拟基础),会有更多 不止一个vptr。 (vptr处于固定偏移处 尊重该类的基地址,并且在该情况下 多重继承,并非所有类都可以具有相同的基础 地址)。

  4. 关于你的最后评论:vtable是在编译时填充的 时间,是静止的。但是vptr是在运行时设置的, 根据动态类型,函数调用使用它 找到vtable并发出电话。

    在你的(非常简单的)例子中,有三个vtable,一个用于 每节课。因为只涉及简单的继承 每个实例只有一个vptr,在BaseBase之间共享 派生类。 Base::f1的vtable将包含四个插槽, 指向Base::f2Base::f3Base::f4Der1Der1::f1的vtable也将包含四个指向的插槽 Der1::f2Base::f3Base::f4Der2。 vtable 对于Base::f1,我会指向Base::f2Der2::f3Der2::f4BaseBase的构造函数将vptr设置为 Base的表格;派生类的构造函数将 首先调用基类的构造函数,然后设置vptr 到与其类型相对应的vtable。 (在实践中,在这样的 简单的情况下,编译器可能能够确定 vptr从未在void f(Base* p) { p->f1(); } 的构造函数中使用,等等 跳过设置它。在更复杂的情况下,编译器所在的位置 无法看到基类构造函数的所有行为, 但事实并非如此。)

    至于为什么它被称为运行时多态,请考虑 功能:

    p

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

答案 1 :(得分:4)

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的调用基本上意味着&#34;在p1的虚拟表&#34;上调用函数0。 vptr[0]Der1::F1,因此会被调用。

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

答案 2 :(得分:2)

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

也许你应该read about that topic firstHere是C ++特有的东西。

答案 3 :(得分:0)

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