关于虚函数的两个问题

Posted

技术标签:

【中文标题】关于虚函数的两个问题【英文标题】:Two questions related to virtual functions 【发布时间】:2011-07-12 14:12:08 【问题描述】:

我在读这篇文章,在这个标题 Inheritance of Base-class vPtrs ,但不明白他在这一段中的意思:

"但是,由于多重继承,一个类可能间接继承自多个类。如果我们决定将所有基类的vtable合并为一个,vtable可能会变得非常大。为了避免这样,编译器不会丢弃所有基类的 vPtrs 和 vtables 并将所有 vtables 合并为一个,而是仅对所有 FIRST 基类执行此操作,并保留所有后续基类及其基类的 vPtrs 和 vtables强>。”

换句话说,在一个对象的内存占用中,您可以在整个层次结构中找到其所有基类的 vPtr,除了所有“首生”。。 em>"

谁能用简单易懂的形式解释一下这一段。

另一个问题,请看这个答案: Follow this answer

现在感兴趣的代码 (*(foo)((void**)(((void**)(&a))[0]))[1])();,谁能告诉我发生了什么? ,特别是为什么在 c++(void**)(&a) 中这样做。我知道这是强制转换,但 &a 返回类型为 void* 的 vptr 的地址(在上述问题链接 2 中)。

谢谢

【问题讨论】:

【参考方案1】:

关于你的第一个问题,我并没有真正理解 引用的段落正在理解;实际上听起来作者没有 真正了解 vtables 是如何工作的(或者在 细节)。当我们谈到 “合并”基类和派生类的 vtable,我们是 谈论使基类 vtable 成为派生类的前缀 桌子;基类 vtable 必须从派生的开头开始 类 vtable 使其工作;两个基础中 vptr 的偏移量 并且派生的必须相同(实际上几乎总是 0),并且 基类必须放在派生类的最开始。和 当然,只有一个基地才能满足这些条件 班级。 (大多数编译器将使用第一个出现在 从左到右扫描代码。)

关于表达式,它是完全未定义的行为,并且 不适用于某些编译器。或者可能会或可能不会工作,具体取决于 优化水平。其中的void* 被用作 任意数量的指针类型的占位符(包括,可能, 指向函数类型的指针)。如果我们采取最里面的部分,我们是 说&a 是指向(1 个或多个)void* 的指针。这个指针是 然后取消引用((X)[0]*(X) 相同,所以 (((void**)(&a))[0])*(void**)(&a) 相同。 ([0] 符号表明这背后可能有更多的价值; IE。 [1] 等也可能有效。这里不是这种情况。)这个 生成void*,然后再转换为void** 取消引用,这次真正使用索引(因为希望 成一个数组);此取消引用的结果将转换为 foo (指向函数的指针),然后将其取消引用并且函数 调用时不带任何参数。

这些都不会真正起作用。它提出了许多假设 这并不总是正确的,或者在某些情况下甚至通常是正确的:

对象中vptr的偏移量为0。(这个一般为真) vptr 本身的大小与void* 相同。 (这几乎总是正确的,并且是 Posix 所要求的。) vtable 本身是一个指向函数的指针数组,并且 指向函数的指针与void* 的大小相同。而同时 确实,指向函数的指针通常具有相同的大小 void*(同样,Posix 需要它,在 Windows 下也是如此), 很难想象如果 vtable 会起作用的实现 只是一个指向函数的指针数组。 调用的函数实际上并没有使用 this 指针: 这仅适用于特殊情况。

他显然在使用 VC++(基于 __thiscall,这是一个 微软主义),我只分析了Sun CC的布局,即 绝对不同。 (而且 Sun CC 和 g++ 也很 不同——就此而言,Sun CC 3.1、Sun CC 4.0 和 Sun CC 5.0 都是不同的。)

除非您实际上正在编写编译器,否则我会忽略所有这些。和 我当然会忽略你引用的表达方式。

【讨论】:

vptrs 与 void * 大小相同,在 windows 下也需要 COM 兼容性。 我认为是,因为&a 将返回地址到它的第一个数据@James:成员(这是我知道的对象的地址),它是指向 void* 的指针,即 vptr(指向 vtable)。所以 &a 的类型不会变成 void** 类型?看到这个:ideone.com/Dfnon。我现在很困惑。让我知道我认为什么是对的? @Mr. Anubis &a 返回对象的地址。除非对象类型是 POD(不是这种情况),否则这不一定与它的第一个数据的地址相同;实际上,它将是 vptr 所在的地址。并且演员不会使&a变成void**;它使编译器将其视为void*【参考方案2】:

虚拟函数调用通常使用virtual method table 实现。编译器通常为程序中的每个类创建一个这样的表,并在实例化对象时给每个对象一个指向与其类对应的虚拟表的指针(该指针称为vptr)。这样,当您调用虚方法时,对象“知道”确切地调用哪个函数,而不管它的静态类型如何。

正如我上面提到的,通常每个类都有自己的虚拟方法表。您引用的段落说,如果一个类派生自例如5 个基类,每个基类都派生自其他 5 个类,那么这个类的虚拟表最终应该是基类的所有 25 个虚拟表的合并版本。这会有点浪费,因此编译器可能决定只将“直接”基类中的 5 个虚拟表合并到派生类的虚拟表中,并将 vptrs 保留为其他 20 个虚拟表作为隐藏存储类中的成员(现在总共有 21 个vptrs)。

这样做的好处是,每次派生具有虚拟表的类时,您都不需要保留重复相同信息的内存。缺点是您使实现复杂化(例如,当调用虚拟方法时,编译器现在必须以某种方式找出vptrs 中的哪一个指向告诉它调用哪个方法的表)。

关于第二个问题,我不确定你到底在问什么。这段代码假定vptr 是该类对象的内存布局中的第一项(这实际上经常是正确的,但是一个可怕的黑客,因为它没有说虚拟方法甚至是使用虚拟实现的表;可能甚至没有vptr),从表中获取第二项(它是指向类的成员函数的指针)并运行它。 p>

即使是最轻微的问题也期待烟花(例如:没有vptrvptr 的结构不是编写代码的人所期望的,更高版本的编译器决定更改方式它存储虚拟表,指向的方法具有不同的签名等)。

更新(解决 cmets)

假设我们有

class Child : Mom, Dad ;

class Mom : GrandMom1, GrandDad1 ;

class Dad : GrandMom2, GrandDad2 ;

在这种情况下,MomDad 是直接基类(“first-born”,但该术语具有误导性)。

【讨论】:

para 中的“firstborns”是什么意思? 从我可以理解的段落来看,我认为 first-born 是指直接的父类,奇怪的是;我猜这个想法是,就编译时需要添加哪些附加类而言,它们是第一个出生的。【参考方案3】:

如果 Derived 继承自 Base,它的 vtable 将扩展 Base 的。现在,如果它也继承了 Base2,它的 vtable 将不包含 Base2 的 - Base2 的部分将保留它的 vtable(使用派生的虚函数更新,如果它们覆盖 Base2 的)。

  Base members      Base2 members    Derived members
+--+------------+----+------------+------------------+
 |                |
 V                V
Derived + Base   Base2 vtable  
vtable

为了让第二个问题更容易理解,因为我喜欢用固定宽度的字体绘图……这是a的内存布局。有关该表达式的完整解释,请参阅 James Kanze 的回答。

        +---+----------+
a:      | | |    A     |
        +-+-+----------+
          |
          V
vtable: +---+
        | --+--> f2()
        +---+
        | --+--> f3()
        +---+

HTH...

【讨论】:

你不觉得你的casting &a to a void* will point you to the first element of the virtual table是错的吗? , 因为&a 将返回地址到它的第一个元素,这是指向 void* (指向 vtable) 的指针。看到这个:ideone.com/Dfnon。我现在很困惑,请帮助说明示例谢谢 &a 转换为void*不会改变它在内存中指向的内容;它仍将指向a 的开头。它将其转换为 void**,然后解除引用,从而获得 vptr(如果一切顺利,并且所有假设都成立,并且编译器没有进行有趣的优化等)。 @James:我的心在哭,我无法理解你在说什么,请逐点给出我能理解的答案。告诉我它是什么,它的意图是什么,它到底做了什么(void**)&a @Mr.Anubis,@James Kanze,现在看,我不知道那个 void* 是如何到达那里的......我已经放弃了我的答案的那部分,因为 James'es反正更好。 @Mr.Anubis,我添加了a 的内存布局图。希望这能帮助你的大脑停止哭泣...... :-)【参考方案4】:

对于问题 1: 我认为这一段只是说vtables实际上并没有合并为一个并存储在乘法派生类的内存分配中;但是第一个基类之外的基类的vtables是通过引用来使用的。

换句话说;如果你有 Rose 是从 Flower 派生的,那么 Rose 只直接包含 Flower 的 vtable,但是 Plant 的 vtable 的使用是通过从 Plant 的 vtable 调用它们来完成的。 )

对于问题 2: 我不太擅长在脑海中做这些,我必须开始将其分解为可管理的块才能理解。

首先我会这样标记它:

(
  *(foo)
  (
    (void**)
    (
      ((void**)(&a))[0]
    )
  )
  [1]
)
(); 

那么,

第 1 步:

((void**)(&a))[0]

我们知道(X)[0] = *X

X = (void**)&a

X[0] = ((void**)&a)[0] = (void*)a

现在替换:

(
  *(foo)
  (
    (void**)
    (
      (void*)(a)
    )
  )
  [1]
)
(); 

第 2 步:

(void**)((void*)(a)) = (void**)(void*)a = (void**)a

(
  *(foo)
  (
    (void**)a
  )
  [1]
)
(); 

第 3 步:

所以看起来我们只剩下一个函数指针

(foo)((void**)a)[1] = (foo)((void*)(a+1))

或者,在位置 (a+1) 处起作用的 void*,类型为 foo...

我认为这至少接近正确 :) 函数指针总是给我带来问题。 ;)

【讨论】:

请问这行是什么意思(void**)(&a) 好吧,就其本身而言,它意味着获取“a”的地址并将其转换为指向 void* 的指针......另一种方式:“a”被视为拥有一个 void *,因此获取 void* 的地址会给您一个 void**。 或者:一个 int* 是一个指向 int 的指针; int** 是一个指向 int 的指针。 “void”在指针类型中使用时是特殊的,void* 是指向未指定/未知类型的指针。所以,一个 void** 只是一个指向某个未知类型的指针。因此,从 (void**)(&a) 我们可以看到,我们将“a”视为指向某个未知类型的指针。如果有帮助: (void**)(&a) 等价于 (void**)&a 和 &((void*)a)。

以上是关于关于虚函数的两个问题的主要内容,如果未能解决你的问题,请参考以下文章

关于虚函数与纯虚函数的区别

关于C++的虚函数在父类的内部调用

关于C++中虚函数表的几点总结

C++面向对象总结:虚指针与虚函数表,干货又来了

C++ 关于类与对象在虚函数表上唯一性问题 浅析

关于虚函数,类的内存分布以及类的成员函数调用原理