以下对于 GCC 是正确的(对于 LLVM链接似乎也是如此),但对于您正在使用的编译器也可能是正确的。所有这些都是依赖于实现的,并且不受 C++ 标准的约束。但是,GCC 编写了自己的二进制标准文档Itanium ABI。
作为我关于 C++ 中的虚函数性能的文章的一部分,我试图用更简单的语言解释虚拟表如何布局的基本概念,您可能会发现这篇文章很有用。以下是您的问题的答案:
描述对象内部表示的更正确方法是:
| vptr | ======= | ======= | <-- your object
|----A----| |
|---------B---------|
B
包含它的基类 A
,它只是在它结束后添加了几个他自己的成员。
从B*
to 转换A*
确实没有任何作用,它返回相同的指针,并且vptr
保持不变。但是,简而言之,虚函数并不总是通过 vtable 调用。有时它们就像其他函数一样被调用。
这里有更详细的解释。您应该区分两种调用成员函数的方式:
A a, *aptr;
a.func(); // the call to A::func() is precompiled!
aptr->A::func(); // ditto
aptr->func(); // calls virtual function through vtable.
// It may be a call to A::func() or B::func().
问题是它在编译时知道如何调用该函数:通过 vtable 或者只是通常的调用。问题是转换表达式的类型在编译时是已知的,因此编译器会在编译时选择正确的函数。
B b, *bptr;
static_cast<A>(b)::func(); //calls A::func, because the type
// of static_cast<A>(b) is A!
在这种情况下,它甚至看不到 vtable 内部!
一般来说,没有。如果一个类从多个基类继承,则可以有多个 vtable,每个基类都有自己的 vtable。这样的一组虚拟表形成了一个“虚拟表组”(参见第 3 部分)。
类还需要一组构造 vtable,以便在构造复杂对象的基础时正确分配虚函数。您可以在我链接的标准中进一步阅读。
这是一个例子。假设C
继承自A
and B
,每个类定义virtual void func()
, 以及a
,b
或c
与其名称相关的虚函数。
将C
有一个包含两个 vtable 的 vtable 组。它将共享一个vtable A
(当前类的自己的函数所在的vtable称为“primary”),并且B
将附加一个vtable:
| C::func() | a() | c() || C::func() | b() |
|---- vtable for A ----| |---- vtable for B ----|
|--- "primary virtual table" --||- "secondary vtable" -|
|-------------- virtual table group for C -------------|
内存中对象的表示看起来几乎与其 vtable 的样子相同。只需vptr
在组中的每个 vtable 之前添加一个,您就可以粗略估计数据在对象内的布局方式。您可以在 GCC 二进制标准的相关部分阅读它。
虚拟基础(其中一些)布置在 vtable 组的末尾。这样做是因为每个类应该只有一个虚拟基础,并且如果它们与“通常”的 vtables 混合在一起,那么编译器就不能重用构造的 vtables 的一部分来制作派生类的部分。这将导致计算不必要的偏移并降低性能。
由于这样的放置,虚拟基础还在它们的 vtables 中引入了额外的元素:vcall
偏移量(当从指针跳转到完整对象内的虚拟基础到覆盖虚拟函数的类的开头时,获取最终覆盖器的地址)对于那里定义的每个虚拟功能。此外,每个虚拟基都添加了vbase
偏移量,这些偏移量被插入到派生类的 vtable 中;它们允许找到虚拟基础数据的开始位置(它不能被预编译,因为实际地址取决于层次结构:虚拟基础位于对象的末尾,并且从开始的移位取决于有多少非虚拟当前类继承的类。)。
哇,我希望我没有引入太多不必要的复杂性。在任何情况下,您都可以参考原始标准,或您自己的编译器的任何文档。