两个与虚函数相关的问题

时间:2011-07-12 14:12:08

标签: c++

我正在读这篇文章,就在这个标题上 Inheritance of Base-class vPtrs,但无法理解他在这段中的意思:

“但是,由于多重继承,一个类可能间接地从许多类继承。如果我们决定将所有基类的vtable合并为一个,则vtable可能变得非常大。 要避免这不是丢弃所有基类的vPtrs和vtable,而是将所有vtable合并为一个,编译器只对所有FIRST基类执行,并保留所有后续基类及其基类的vPtrs和vtable “。

换句话说,在对象的内存占用中,您可以在层次结构中找到所有基类的vPtrs,除了所有“先发制人”

任何人都可以用简单易懂的形式解释这一段。

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

现在感兴趣的代码 (*(foo)((void**)(((void**)(&a))[0]))[1])();,任何人都可以告诉我们发生了什么事吗? ,特别是为什么这完全用c ++ (void**)(&a)完成。我知道这是转换,但&a返回vptr的地址(在上面的问题链接2中),其类型为void *。

由于

4 个答案:

答案 0 :(得分:2)

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

正如我上面提到的,通常每个类都有自己的虚方法表。你引用的段落说如果一个类派生自例如5个基类,每个基类也派生自5个其他类,然后该类的虚拟表最终应该是基类的所有25个虚拟表的合并版本。这将是一种浪费,因此编译器可能决定仅将“立即”基类中的5个虚拟表合并到派生类的虚拟表中,并将vptr s保存为存储为其他20个虚拟表的虚拟表类中的隐藏成员(现在总共有vptr个。)

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

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

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

更新(解决评论)

假设我们有

class Child : Mom, Dad {};

class Mom : GrandMom1, GrandDad1 {};

class Dad : GrandMom2, GrandDad2 {};

在这种情况下,MomDad是直接基类(“第一次出生”,但该术语具有误导性。)

答案 1 :(得分:2)

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

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

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

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

... HTH

答案 2 :(得分:2)

关于你的第一个问题,我并不真正关注 引用的段落即将到来;它实际上听起来像作者没有 真的了解vtable是如何工作的(或者没有考虑过它 详情)。当我们谈到时,重要的是要意识到这一点 “合并”基础和派生类的vtable,我们是 谈论使基类vtable成为派生类的前缀 表;基类vtable必须从派生的开始处开始 为此工作的班级vtable;两个基数中的vptr的偏移量 派生必须相同(在实践中几乎总是0),和 基类必须放在派生的最开头。和 当然,只有一个基地才能满足这些条件 类。 (大多数编译器将使用出现的第一个非虚拟基础 从左到右扫描代码。)

关于表达式,它是完全未定义的行为,并且 不适用于某些编译器。或者可能会或可能不会起作用,具体取决于 优化程度。并且其中的void*被用作 任意数量的指针类型的占位符(可能包括 指向函数类型的指针)。如果我们采取最内在的部分,我们就是 说&a是指向(1或更多)void*的指针。这个指针是 然后解除引用((X)[0]*(X)相同,所以 (((void**)(&a))[0])*(void**)(&a)相同。 ([0] 符号表明这个背后可能有更多的价值观;即 [1]等也可能有效。这不是这种情况。)这个 结果为void*,然后再次投放到void** dereferenced,这次真的使用索引(因为它有希望 成阵列);此解除引用的结果将转换为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 各有不同。)

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

答案 3 :(得分:0)

问题1: 我认为段落只是说vtable实际上并没有合并成一个并存储在多个派生类的内存分配中;但是引用使用超出第一个基类的基类的vtable。

换言之;如果你有来自植物的玫瑰来自植物,那么玫瑰只能直接使用Flower的vtable,但是植物的vtable的使用是通过从植物的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))

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

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