【问题标题】:How does the C++ compiler know which implementation of a virtual function to call?C++ 编译器如何知道要调用哪个虚函数实现?
【发布时间】:2010-09-17 05:12:30
【问题描述】:

这是来自http://www.cplusplus.com/doc/tutorial/polymorphism.html 的多态性示例(为便于阅读而编辑):

// abstract base class
#include <iostream>
using namespace std;

class Polygon {
    protected:
        int width;
        int height;
    public:
        void set_values(int a, int b) { width = a; height = b; }
        virtual int area(void) =0;
};

class Rectangle: public Polygon {
    public:
        int area(void) { return width * height; }
};

class Triangle: public Polygon {
    public:
        int area(void) { return width * height / 2; }
};

int main () {
    Rectangle rect;
    Triangle trgl;
    Polygon * ppoly1 = &rect;
    Polygon * ppoly2 = &trgl;
    ppoly1->set_values (4,5);
    ppoly2->set_values (4,5);
    cout << ppoly1->area() << endl; // outputs 20
    cout << ppoly2->area() << endl; // outputs 10
    return 0;
}

我的问题是编译器如何知道 ppoly1 是一个 Rectangle 而 ppoly2 是一个三角形,以便它可以调用正确的 area() 函数?它可以通过查看“Polygon * ppoly1 = &rect;”来发现这一点line 并且知道 rect 是一个 Rectangle,但这并不是在所有情况下都有效,不是吗?如果你做了这样的事情怎么办?

cout << ((Polygon *)0x12345678)->area() << endl;

假设您被允许访问该随机内存区域。

我会对此进行测试,但我目前无法在我使用的计算机上进行测试。

(我希望我没有遗漏一些明显的东西......)

【问题讨论】:

  • Offtopic:为什么不给那些花时间为你写有用答案的人投票呢?

标签: c++ oop polymorphism


【解决方案1】:

忽略绑定的各个方面,实际上并不是编译器决定了这一点。

C++ 运行时通过 vtable 和 vpointers 评估派生对象在运行时的实际情况。

我强烈推荐 Scott Meyer 的《Effective C++》一书,它很好地描述了这是如何完成的。

甚至涵盖了如何忽略派生类中的方法中的默认参数以及仍然采用基类中的任何默认参数!那是有约束力的。

【讨论】:

    【解决方案2】:

    每个对象(属于具有至少一个虚函数的类)都有一个指针,称为vptr。它指向其实际类的vtbl(每个具有虚函数的类至少有一个;对于某些多重继承场景可能不止一个)。

    vtbl 包含一堆指针,每个指针对应一个虚函数。所以在运行时,代码只是使用对象的vptr 来定位vtbl,并从那里找到实际覆盖函数的地址。

    在您的特定情况下,PolygonRectangleTriangle 每个都有一个 vtbl,每个都有一个指向其相关 area 方法的条目。你的ppoly1 将有一个vptr 指向Rectanglevtblppoly2Trianglevtbl 类似。希望这会有所帮助!

    【讨论】:

    • vptr/vtbl。 Rally 我不记得标准中的那些:-) 指向 vtable 的指针。其中 vtable 是编译器定义的结构更具描述性。
    • @Martin:vptr/vtbl 是 Bjarne Stroustrup 的《C++ 编程语言》一书中使用的术语。 :-)
    • 我猜 vtable 不是标准所要求的,碰巧大多数编译器都使用一个 vtable 实现多态性,因此它或多或少成为标准行为
    • vtable 也不在标准中。这只是可能的许多实现中的一种。它恰好是几乎普遍使用的一种,但标准并未强制要求它。 (出于好奇 - 是否有一个可访问的编译器可以做其他事情?)
    • 我同意这是一个实现细节,但我发现它实际上可以帮助人们理解虚函数是如何工作的。 :-)
    【解决方案3】:

    回答您问题的第二部分:该地址可能不会在正确的位置有一个 v-table,然后会发疯。而且,根据标准,它是未定义的。

    【讨论】:

      【解决方案4】:

      Chris Jester-Young 给出了这个问题的基本答案。

      Wikipedia 有更深入的处理。

      如果您想了解此类事物的工作原理(以及所有类型的继承,包括多重继承和虚拟继承)的全部详细信息,最好的资源之一是 Stan Lippman 的“Inside the C++ Object Model”。

      【讨论】:

        【解决方案5】:
        cout << ((Polygon *)0x12345678)->area() << endl;
        

        这段代码是一场等待发生的灾难。编译器会正常编译它,但在运行时,你不会指向一个有效的 v-table,如果你幸运的话,程序就会崩溃。

        在 C++ 中,您不应该像这样使用旧的 C 风格强制转换,您应该像这样使用 dynamic_cast

        Polygon *obj = dynamic_cast<Polygon *>(0x12345678)->area();
        ASSERT(obj != NULL);
        
        cout << obj->area() << endl;
        

        如果给定的指针不是有效的 Polygon 对象,dynamic_cast 将返回 NULL,因此它将被 ASSERT 捕获。

        【讨论】:

        • 你不能从整数进行动态转换! (事实上​​,您也不能从 void* 进行动态转换,您必须从与您要转换的类型有某种关系的类型的指针/引用开始。)
        • 所以我的意思是,对于像 reinterpret_cast(0x12345678) 这样的随机地址,无论如何你都处于未定义的行为区域。 :-P
        • 我实际上已经做过这种事情来将对象指针存储在 Windows 列表框中。诚然,我必须这样写: MYTYPE *obj = dynamic_cast((MYTYPE *)listbox.GetItemData(item)); dynamic_cast 比直接投射更安全。
        • 如果 GetItemData 返回 void*,您可以改用 static_cast 来避免 C 样式的强制转换。 :-) 我认识的一些人对避免 C 风格的强制转换非常教条,因为它们可能是一个相当生硬的工具(例如,在意外情况下会导致 reinterpret_cast)。
        • 另一方面,如果 GetItemData 返回一个 int,那么 reinterpret_casting 它到一个指针不是 64 位安全的,在这种情况下我可能会使用 map。 :-)
        【解决方案6】:

        虚函数表。也就是说,您的两个 Polygon 派生对象都有一个虚函数表,其中包含指向所有(非静态)函数实现的函数指针;当你实例化一个 Triangle 时, area() 函数的虚函数指针指向 Triangle::area() 函数;当你实例化一个 Rectangle 时, area() 函数指向 Rectangle::area() 函数。由于虚函数指针与对象的数据一起存储在内存中,因此每次将该对象作为多边形引用时,都会使用该对象的相应 area()。

        【讨论】:

          猜你喜欢
          • 2021-10-27
          • 2016-01-05
          • 1970-01-01
          • 1970-01-01
          • 2013-05-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多