虚拟调度实施细节

时间:2010-10-19 20:36:28

标签: c++ vtable vptr

首先,我想清楚地表明我确实理解在C ++标准中没有vtable和vptrs的概念。但是我认为几乎所有实现都以几乎相同的方式实现虚拟调度机制(如果我错了,请纠正我,但这不是主要问题)。另外,我相信我知道虚函数是如何工作的,也就是说,我总能告诉我将调用哪个函数,我只需要实现细节。

假设有人问我以下内容:
 “您的基类B具有虚函数v1,v2,v3和派生类D:B,它们会覆盖函数v1和v3并添加虚函数v4。解释虚拟调度的工作原理”。

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

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

D的vtable将包含

&D::v1
&B::v2
&D::v3
&D::v4 

现在B类包含一个成员指针vptr。 D自然地继承它,因此也包含它。在B B的构造函数和析构函数中,将vptr设置为指向B的vtable。在D D的构造函数和析构函数中,它指向D的vtable 在多态类X的对象x上对虚函数f的任何调用都被解释为对x.vptr [f在vtable中的位置]的调用

问题是:
1.我在上述说明中是否有任何错误?
2.编译器如何知道f在vtable中的位置(请详细说明)
这是否意味着如果一个班级有两个基地,那么它有两个vptrs?在这种情况下发生了什么? (尝试以与我相似的方式描述,尽可能详细地说明)
4.钻石层次结构中发生了什么,A在顶部B,C在中间,D在底部? (A是B和C的虚拟基类)

提前致谢。

3 个答案:

答案 0 :(得分:35)

<强> 1。我在上述说明中有任何错误吗?

一切都好。 : - )

<强> 2。编译器如何知道f在vtable中的位置

每个供应商都有自己的方式,但我总是将vtable视为成员函数签名到内存偏移的映射。所以编译器只维护这个列表。

第3。这是否意味着如果一个类有两个基础,那么它有两个vptrs?在这种情况下发生了什么?

通常,编译器组成一个 new vtable,它包含按指定顺序附加在一起的虚拟碱基的所有vtable,以及虚拟碱基的vtable指针。它们遵循派生类的vtable函数。这特定于供应商,但对于class D : B1, B2,您通常会看到D._vptr[0] == B1._vptr

multiple inheritance

该图像实际上是用于组成对象的成员字段,但是vtables可以由编译器以完全相同的方式组成(据我所知)。

<强> 4。在钻石层次结构中发生了什么,A在顶部B,C在中间,D在底部? (A是B和C的虚拟基类)

简短的回答?绝对的地狱。你真的继承了这两个基地吗?只是其中之一?他们都不是?最终,使用了为该类组成vtable的相同技术,但是这样做的方式各不相同,因为如何应该完成它并不是一成不变的。解决钻石等级问题here有一个很好的解释,但是,与大多数问题一样,它是特定于供应商的。

答案 1 :(得分:5)

  1. 对我好看
  2. 具体实现,但大多数只是源代码顺序 - 意味着它们出现在类中的顺序 - 从基类开始,然后从派生类添加新的虚函数。只要编译器具有确定性的方法,那么它想做的任何事情都可以。但是,在Windows上,要创建COM兼容的V-Tables,必须按源顺序

  3. (不确定)

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

答案 2 :(得分:0)

评论:

  • 我认为析构函数不会进入它!

  • 一个电话,例如D d; d.v1();可能不会通过vtable实现,因为编译器可以在编译/链接时解析函数地址。

  • 编译器知道f的位置,因为它把它放在那里!

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

  • Scott Meyers的“Effective C ++”书籍比我更能解释多重继承和钻石;我建议为此(以及许多其他原因)阅读它们。考虑一下必要的阅读!