39

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

假设有人问我以下问题:
“您有带有虚函数 v1、v2、v3 的基类 B 和派生类 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 自然地继承了它,因此也包含它。在 BB 的构造函数和析构函数中设置 vptr 指向 B 的 vtable。在DD的构​​造函数和析构函数中设置它指向D的vtable。
任何对多态类 X 的对象 x 的虚函数 f 的调用都被解释为对 x.vptr[f 在 vtables 中的位置] 的调用

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

提前致谢。

4

3 回答 3

37

1.我上面的描述有什么错误吗?

都好。:-)

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

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

3. 这是否意味着如果一个类有两个基数,那么它就有两个 vptr?在这种情况下发生了什么?

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

多重继承

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

4. A 在顶部 B,C 在中间,D 在底部的菱形层次结构中发生了什么?(A 是 B 和 C 的虚拟基类)

简短的回答?绝对的地狱。你实际上继承了这两个基地吗?只有其中之一?他们都不是?最终,使用了为该类编写 vtable 的相同技术,但是如何做到这一点的方式千差万别,因为应该如何做到这一点根本不是一成不变的。这里有一个关于解决菱形层次问题的不错的解释,但是,就像大多数情况一样,它是特定于供应商的。

于 2010-10-19T21:07:53.873 回答
5
  1. 对我来说看上去很好
  2. 具体实现,但大多数只是按源代码顺序——即它们出现在类中的顺序——从基类开始,然后从派生类中添加新的虚函数。只要编译器有确定的方式来做这件事,那么它想做的任何事情都可以。但是,在 Windows 上,要创建与 COM 兼容的 V-Table,它必须按源顺序排列

  3. (没有把握)

  4. (猜测)菱形只是意味着您可以拥有基类 B 的两个副本。虚拟继承会将它们合并到一个实例中。所以如果你通过 D1 设置成员,你可以通过 D2 读取它。(其中 C 源自 D1、D2,它们中的每一个都源自 B)。我相信在这两种情况下,vtables 都是相同的,因为函数指针是相同的——数据成员的内存是被合并的。
于 2010-10-19T20:50:41.510 回答
1

评论:

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

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

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

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

  • Scott Meyers 的“Effective C++”书籍比我更好地解释了多重继承和菱形;我建议出于这个(以及许多其他)原因阅读它们。把它们当成必不可少的读物!

于 2010-10-19T20:50:02.897 回答