虚函数调用的实现细节

43

首先,我想明确一点,我知道C++标准中没有虚函数表和虚函数指针的概念。然而,我认为几乎所有的实现都以非常相似的方式实现了虚函数调用机制(如果我错了,请纠正我,但这不是主要问题)。此外,我相信我知道虚函数如何工作,也就是说,我可以总是知道哪个函数将被调用,我只需要实现细节。

假设有人问我以下问题:
"你有一个带有虚函数v1、v2、v3的基类B和一个派生类D:B,它覆盖了函数v1和v3并添加了一个虚函数v4。请解释虚调度的工作原理。"

我的回答如下:
对于每个具有虚函数的类(在本例中为B和D),我们有一个单独的指向函数的指针数组,称为虚函数表。
B的虚函数表将包含

&B::v1
&B::v2
&B::v3

D的虚函数表将包含以下内容

&D::v1
&B::v2
&D::v3
&D::v4 
现在类B包含一个成员指针vptr。D自然继承它,因此也包含它。在B的构造函数和析构函数中,B将vptr设置为指向B的虚表。在D的构造函数和析构函数中,D将其设置为指向D的虚表。
对于多态类X的对象x上的任何虚函数f的调用都被解释为对x.vptr[f在虚表中的位置]的调用。
问题如下:
1. 上述描述是否有错误?
2. 编译器是如何知道f在虚表中的位置的(详细说明)?
3. 这是否意味着如果一个类有两个基类,则它有两个vptrs?在这种情况下会发生什么?(尽可能详细地描述)
4. 在一个顶部为A,中间为B、C,底部为D的菱形继承中会发生什么?(A是B和C的虚基类)
谢谢。
3个回答

39

1. 在上述描述中是否有任何错误?

一切都好。:-)

2. 编译器如何知道vtable中f的位置?

每个供应商都有自己的方法,但我总是把vtable看作是将成员函数签名映射到内存偏移量的地图。因此,编译器只需维护此列表。

3. 这是否意味着如果一个类有两个基类,则它有两个vptrs?在这种情况下会发生什么?

通常,编译器会组合一个新的vtable,其中包含按照指定顺序附加在一起的所有虚拟基类的vtable,以及虚拟基类的vtable指针。然后加上派生类的vtable函数。这是极其与供应商相关的,但对于class D : B1, B2,你通常会看到D._vptr[0] == B1._vptr

multiple inheritance

该图实际上是用于组合对象的成员字段,但是编译器可以按照完全相同的方式组合vtable(就我所知)。

4. 在一个菱形继承体系中,A在顶部,B、C在中间,D在底部时会发生什么?(A是B和C的虚拟基类)

简短的答案?绝对混乱。你是否虚拟继承了两个基类?只有其中一个?都没有?最终,使用相同的组合类vtable的技巧,但如何做到这一点因为应该如何做并没有确定而变化得太过极端。关于解决菱形继承问题有一个不错的解释在这里,但像大多数情况一样,这也非常供应商特定。


@Armen:问题在于我将cleanup定义为纯虚函数,这意味着无法调用它。 - Travis Gockel
@David:这对我来说是个新闻。背后的理由是什么? - Oliver Charlesworth
@David:我明白了。所以并不是动态分派没有发生,而是它不会分派到当前正在进行析构函数覆盖的子对象? - Oliver Charlesworth
2
@Armen,@Oli:我在codepad上写了一个小片段。这是我第一次在那里发布帖子,如果不能正常工作,请告诉我,我会在其他地方提供代码。 - David Rodríguez - dribeas
1
“Base1”被称为“主要基础”。 - curiousguy
显示剩余10条评论

5
  1. 看起来不错
  2. 实现方式各有不同,但大多数只是按照源代码顺序排列——也就是按照类中出现的顺序开始,从基类开始,然后添加派生类中的新虚拟函数。只要编译器有确定性的方法来执行此操作,那么它想做什么都可以。但是在Windows上,为了创建与COM兼容的V-Table,必须按照源代码顺序排列。

  3. (不确定)

  4. (猜测) 钻石表示您可以拥有两个基类B的副本。虚继承将它们合并为一个实例。因此,如果您通过D1设置成员,则可以通过D2读取它(其中C派生自D1、D2,它们都派生自B)。我相信,在这两种情况下,vtable都是相同的,因为函数指针是相同的——数据成员的内存是被合并的。

抱歉,我对第三和第四个问题没有确定的答案。我认为有一步是确保您拥有正确的虚函数表,但我不知道具体细节。 - Lou Franco

1

评论:

  • 我认为析构函数与此无关!

  • 例如,D d; d.v1(); 这样的调用可能不会通过虚函数表实现,因为编译器可以在编译/链接时解析函数地址。

  • 编译器知道 f 的位置是因为它自己放在那里!

  • 是的,一个有多个基类的类通常会有多个虚指针(假设每个基类中都有虚函数)。

  • Scott Meyers 的 "Effective C++" 书籍比我更好地解释了多重继承和菱形继承问题;我推荐阅读这些书籍,因为它们是必读的!


@Oli:关于第一点 - 它们确实会这样做,否则当在Base的析构函数中调用虚函数f时,为什么会调用Base :: f而不是Derived :: f? - Armen Tsirunyan
他是正确的,析构函数不会进行虚拟调用。为了在一个调用中实现这一点,它可能会像他描述的那样替换vtable(但要注意这都是特定于实现的)。 - Lou Franco
@Oli:顺便说一句,我已经读过梅耶斯(Meyers)的书,并在发布此帖子之前查阅了相关章节,但他并没有完全回答我具体的问题。 - Armen Tsirunyan
@Armen:在《More Effective C++》的第24项中,他给出了一种继承菱形的vptrs的可能实现。因为实现者会略有不同,所以没有一个明确的答案! - Oliver Charlesworth
1
@Armen:在派生类中,根据对象的动态类型可能会调用覆盖。在一个层次结构base <- derived <- rederived中,当类型为rederived的对象完成~rederived后,对象的动态类型derived - David Rodríguez - dribeas
显示剩余5条评论

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