C++编译器如何知道调用哪个虚函数的实现?

11

以下是关于多态性的示例,来源于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是一个矩形,ppoly2是一个三角形,以便它可以调用正确的area()函数?编译器可以通过查看"Polygon * ppoly1 = ▭"这一行来判断rect是一个矩形,但这种方法并不适用于所有情况,对吗?如果你像下面这样做会怎么样呢?

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

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

我想测试一下,但我现在使用的计算机不行。

(我希望我没有漏掉什么显而易见的东西...)


3
离题:为什么不给那些花时间为你写出有用答案的人投票呢? - Mike F
6个回答

27

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

vtbl 包含一堆指针,每个虚函数一个。因此在运行时,代码只需使用对象的 vptr 来定位 vtbl,然后从那里获取实际覆盖函数的地址。

在您的特定情况下,PolygonRectangleTriangle 各自具有一个 vtbl,每个表中都有一个条目指向其相关的 area 方法。您的 ppoly1 将具有指向 Rectanglevtblvptr,而 ppoly2 类似地具有指向 Trianglevtblvptr。希望这能帮助!


vptr/vtbl。我真的不记得标准中有这些了 :-) 指向虚函数表的指针。虚函数表是编译器定义的结构,更具描述性。 - Martin York
@Martin:vptr/vtbl是Bjarne Stroustrup的书《C++程序设计语言》中使用的术语。 :-) - C. K. Young
我猜标准并不需要vtable,只是大多数编译器使用它来实现多态性,所以它已经成为了更或多少的标准行为。 - 1800 INFORMATION
另一个常用的方法是使用方法名的哈希表。 - 1800 INFORMATION
呃!不是方法名的哈希表!听起来像是IDispatch的后期绑定(或者无论它叫什么,我指的是通过方法名查找函数指针而不是DispIDs的那个)。如果你从未尝试过OLE自动化并且不知道我在说什么,请原谅。:-P - C. K. Young
显示剩余4条评论

6

Chris Jester-Young简要回答了这个问题。

维基百科对此进行了更深入的讨论。

如果您想了解此类事物的全部细节(以及所有类型的继承,包括多重和虚拟继承),最好的资源之一是Stan Lippman的《C++对象模型内幕》。


3

忽略绑定方面的因素,实际上并不是编译器决定这个。

是C++运行时通过虚表和虚指针在运行时评估派生对象的实际类型。

我强烈推荐斯科特·迈耶的《Effective C++》一书,其中有关于如何完成这个过程的良好描述。

甚至涵盖了派生类中方法的默认参数被忽略的情况,任何基类中的默认参数仍然有效!那就是绑定。


1
回答你问题的第二部分:那个地址可能没有正确位置的虚函数表,会导致混乱。而且根据标准来说是未定义的。

1

虚函数表。换句话说,您的两个派生自Polygon的对象都有一个虚函数表,其中包含指向它们所有(非静态)函数实现的函数指针;当您实例化一个Triangle时,area()函数的虚函数指针指向Triangle::area()函数;当您实例化矩形时,area()函数指向Rectangle::area()函数。因为虚函数指针与对象的数据一起存储在内存中,所以每次将该对象视为多边形时,将使用该对象的适当area()。


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

这段代码等着出问题了。编译器可能会编译它,但在运行时,你将无法指向有效的虚函数表,如果你幸运的话,程序只会崩溃。

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

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

cout << obj->area() << endl;

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


你不能从整数进行dynamic_cast转换!(事实上,你也不能从void*进行dynamic_cast转换,你必须从一个与你要转换的类型有某种关系的指针/引用类型开始。) - C. K. Young
所以我的观点是,无论如何,使用类似reinterpret_cast<void*>(0x12345678)这样的随机地址都会进入未定义行为区域。 :-P - C. K. Young
实际上,我已经做过这样的事情,将对象指针存储在Windows列表框中。不可否认,我必须写成这样:MYTYPE * obj = dynamic_cast <MYTYPE *> ((MYTYPE *) listbox.GetItemData(item)); dynamic_cast 比直接强制转换更安全一些。 - Adam Pierce
如果GetItemData返回void*,您可以使用static_cast<MYTYPE*>来避免使用C风格的转换。 :-) 我认识的一些人非常固执地避免使用C风格的转换,因为它们可能是一种很笨重的工具(例如,在意外情况下可能会导致reinterpret_cast)。 - C. K. Young
另一方面,如果 GetItemData 返回一个 int ,那么将其 reinterpret_casting 为指针就不是 64 位安全的,这种情况下我可能会使用 map<int, MYTYPE*>。 :-) - C. K. Young

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接