关于虚函数的两个问题
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 个虚拟表合并到派生类的虚拟表中,并将 vptr
s 保留为其他 20 个虚拟表作为隐藏存储类中的成员(现在总共有 21 个vptr
s)。
这样做的好处是,每次派生具有虚拟表的类时,您都不需要保留重复相同信息的内存。缺点是您使实现复杂化(例如,当调用虚拟方法时,编译器现在必须以某种方式找出vptr
s 中的哪一个指向告诉它调用哪个方法的表)。
关于第二个问题,我不确定你到底在问什么。这段代码假定vptr
是该类对象的内存布局中的第一项(这实际上经常是正确的,但是一个可怕的黑客,因为它没有说虚拟方法甚至是使用虚拟实现的表;可能甚至没有vptr
),从表中获取第二项(它是指向类的成员函数的指针)并运行它。 p>
即使是最轻微的问题也期待烟花(例如:没有vptr
,vptr
的结构不是编写代码的人所期望的,更高版本的编译器决定更改方式它存储虚拟表,指向的方法具有不同的签名等)。
更新(解决 cmets):
假设我们有
class Child : Mom, Dad ;
class Mom : GrandMom1, GrandDad1 ;
class Dad : GrandMom2, GrandDad2 ;
在这种情况下,Mom
和 Dad
是直接基类(“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)。以上是关于关于虚函数的两个问题的主要内容,如果未能解决你的问题,请参考以下文章