我正在读这篇文章,就在这个标题上 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 *。
由于
答案 0 :(得分:2)
虚拟函数调用通常使用virtual method table实现。编译器通常为程序中的每个类创建一个这样的表,并在实例化对象时为每个对象指向与其类对应的虚拟表(该指针称为vptr
)。这样,当您调用虚方法时,无论其静态类型如何,对象都“确切地”知道要调用哪个函数。
正如我上面提到的,通常每个类都有自己的虚方法表。你引用的段落说如果一个类派生自例如5个基类,每个基类也派生自5个其他类,然后该类的虚拟表最终应该是基类的所有25个虚拟表的合并版本。这将是一种浪费,因此编译器可能决定仅将“立即”基类中的5个虚拟表合并到派生类的虚拟表中,并将vptr
s保存为存储为其他20个虚拟表的虚拟表类中的隐藏成员(现在总共有vptr
个。)
这样做的好处是,每次派生带有虚拟表的类时,您都不需要保留重复相同信息的内存。缺点是你使实现复杂化(例如,在调用虚方法时,编译器现在必须以某种方式确定哪个vptr
指向表,告诉它调用哪个方法。)
关于第二个问题,我不确定你到底在问什么。这段代码假设vptr
是该类对象的内存布局中的第一个项目(实际上经常是这样,但是一个可怕的黑客,因为它无处可说它甚至实现了使用虚拟方法一个虚拟表;可能甚至不是一个vptr
),从表中获取第二个项目(这是指向该类成员函数的指针)并运行它
即使是最轻微的事情也会出现烟火(例如:没有vptr
,vptr
的结构不是编写代码的人所期望的,编译器的更高版本决定改变它存储虚拟表的方式,指向的方法有不同的签名等等。)
更新(解决评论):
假设我们有
class Child : Mom, Dad {};
class Mom : GrandMom1, GrandDad1 {};
class Dad : GrandMom2, GrandDad2 {};
在这种情况下,Mom
和Dad
是直接基类(“第一次出生”,但该术语具有误导性。)
答案 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
(指向函数的指针),然后解除引用和函数
在没有任何参数的情况下被调用。
这些都不会真正起作用。它产生了许多假设 这些并不总是,或者在某些情况下甚至是普遍的:
void*
的大小相同。 (这几乎总是正确的,并且需要Posix。)void*
具有相同的大小。而且
确实,指向函数的指针通常具有相同的大小
void*
(同样,Posix需要它,在Windows下也是如此),
很难想象一个如果vtable可以工作的实现
只是一系列功能指针。他显然正在使用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)处起作用的无效* ...
我认为至少接近正确:)函数指针总是给我带来问题。 ;)