我对 vptr 和内存中对象的表示有点困惑,希望你能帮助我更好地理解这个问题。
考虑B
继承自A
并且都定义了虚函数f()
。据我所知,B类对象在内存中的表示如下:[ vptr | A | B ]
和vtbl
that vptr
指向包含B::f()
。我还了解到从B
to A
除了忽略之外什么也不做B
位于对象末尾的部分。这是真的吗?这种行为难道不是错误的吗?我们想要该类型的对象A
执行A::f()
方法而不是B::f()
.
有没有几个vtables
系统中有多少类?
如何将一个vtable
从两个或多个类继承的类是什么样子的? C的对象在内存中如何表示?
与问题 3 相同,但具有虚拟继承。
以下对于 GCC 来说是正确的(对于 LLVM 来说似乎也是正确的)link https://llvm.org/docs/CompilerWriterInfo.html#abi),但对于您正在使用的编译器也可能如此。所有这些都依赖于实现,并且不受 C++ 标准管辖。然而,GCC编写了自己的二进制标准文档,安腾ABI http://static.coldattic.info/cxx-abi/abi.html.
我尝试用更简单的语言解释虚拟表如何布局的基本概念,作为我的一部分关于 C++ 中的虚函数性能的文章 http://coldattic.info/post/3/,您可能会发现这很有用。以下是您的问题的答案:
-
描述对象内部表示的更正确方法是:
| vptr | ======= | ======= | <-- your object
|----A----| |
|---------B---------|
B
contains它的基类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 内部!
-
一般来说,不会。如果一个类继承自多个基类,则该类可以拥有多个虚函数表,并且每个基类都有自己的虚函数表。这样的一组虚拟表形成一个“虚拟表组”(参见第 3 部分)。
类还需要一组构造 vtable,以便在构造复杂对象的基类时正确分配虚拟函数。您可以进一步阅读我链接的标准 http://static.coldattic.info/cxx-abi/abi.html#vtable-ctor.
-
这是一个例子。认为C
继承自A
and B
,每个类定义virtual void func()
, 也a
,b
or c
虚函数与其名称相关。
The C
将有一个由两个 vtable 组成的 vtable 组。它将与以下对象共享一个 vtable:A
(当前类自己的函数所在的vtable称为“primary”),以及一个vtableB
将附加:
| C::func() | a() | c() || C::func() | b() |
|---- vtable for A ----| |---- vtable for B ----|
|--- "primary virtual table" --||- "secondary vtable" -|
|-------------- virtual table group for C -------------|
对象在内存中的表示形式看起来几乎与它的虚函数表相同。只需添加一个vptr
在组中的每个 vtable 之前,您将粗略估计数据在对象内部的布局方式。您可以在相关部分 http://static.coldattic.info/cxx-abi/abi.html#layoutGCC 二进制标准。
-
虚拟库(其中一些)布置在 vtable 组的末尾。这样做是因为每个类应该只有一个虚拟基,如果它们与“通常的”虚函数表混合在一起,那么编译器就无法重用构造的虚函数表的一部分来生成派生类的虚函数表。这将导致计算不必要的偏移并降低性能。
由于这样的放置,虚拟基地还在其 vtable 中引入了附加元素:vcall
此处定义的每个虚拟函数的偏移量(当从完整对象内的虚拟基指针跳转到覆盖虚拟函数的类的开头时,获取最终覆盖程序的地址)。每个虚拟基地还添加vbase
偏移量,插入到派生类的 vtable 中;它们允许找到虚拟基址的数据开始的位置(它不能被预编译,因为实际地址取决于层次结构:虚拟基址位于对象的末尾,并且从开始的移位取决于有多少个非虚拟基址)当前类继承的类。)。
Woof,我希望我没有引入太多不必要的复杂性。无论如何,您可以参考原始标准,或您自己的编译器的任何文档。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)