多重虚拟继承中的虚拟表和内存布局
Posted
技术标签:
【中文标题】多重虚拟继承中的虚拟表和内存布局【英文标题】:Virtual tables and memory layout in multiple virtual inheritance 【发布时间】:2012-07-22 19:07:48 【问题描述】:考虑以下层次结构:
struct A
int a;
A() f(0);
A(int i) f(i);
virtual void f(int i) cout << i;
;
struct B1 : virtual A
int b1;
B1(int i) : A(i) f(i);
virtual void f(int i) cout << i+10;
;
struct B2 : virtual A
int b2;
B2(int i) : A(i) f(i);
virtual void f(int i) cout << i+20;
;
struct C : B1, virtual B2
int c;
C() : B1(6),B2(3),A(1)
virtual void f(int i) cout << i+30;
;
C
实例的确切内存布局是什么?它包含多少个 vptr,每个 vptr 的确切放置位置?哪些虚拟表与 C 的虚拟表共享?每个虚拟表究竟包含什么?
我是如何理解布局的:
----------------------------------------------------------------
|vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a |
----------------------------------------------------------------
其中AptrOfBx
是指向Bx
包含的A
实例的指针(因为继承是虚拟的)。
那是对的吗? vptr1
指向哪些函数? vptr2
指向哪些函数?
给定以下代码
C* c = new C();
dynamic_cast<B1*>(c)->f(3);
static_cast<B2*>(c)->f(3);
reinterpret_cast<B2*>(c)->f(3);
为什么所有对f
的调用都打印33
?
【问题讨论】:
这是作业,还是好奇? 其实这是考试。但我敢肯定,如果我最终理解了这个例子中的事情是如何工作的,我就能理解与多重继承和虚拟继承相关的任何事情。 您可以像这样轻松找出每个父子对象的起点:C foo; intptr_t offsetB1 = (intptr_t)(B1*)&foo - (intptr_t)&foo;
,其他基础的起点可以类似推导出。此外,计算所有类的sizeof
应该会给你另一个很好的线索。
【参考方案1】:
虚拟基地与普通基地有很大不同。请记住,“虚拟”意味着“在运行时确定”——因此整个基础子对象必须在运行时确定。
假设您正在获得B & x
引用,并且您的任务是查找A::a
成员。如果继承是真实的,那么B
有一个超类A
,因此您通过x
查看的B
-object 有一个A
-subobject,您可以在其中找到您的成员A::a
.如果x
的最派生对象有多个A
类型的基,那么您只能看到作为B
子对象的特定副本。
但是如果继承是虚拟的,那么这一切都没有意义。我们不知道我们需要哪个A
-subobject——这些信息在编译时根本不存在。我们可以处理像B y; B & x = y;
中的实际B
-object,或者像C z; B & x = z;
这样的C
-object,或者完全不同的东西,实际上是从A
派生的更多次。唯一知道的方法是在运行时找到实际的基础A
。
这可以通过多一层运行时间接来实现。 (请注意,与非虚拟函数相比,这与虚拟 函数 是如何通过额外的运行时间接实现完全平行的。)一种解决方案不是使用指向 vtable 或基本子对象的指针,而是将指针 存储到指向实际基础子对象的指针。这有时被称为“thunk”或“trampoline”。
所以实际对象C z;
可能如下所示。内存中的实际排序取决于编译器并且不重要,并且我已经抑制了 vtables。
+-+------++-+------++-----++-----+
|T| B1 ||T| B2 || C || A |
+-+------++-+------++-----++-----+
| | |
V V ^
| | +-Thunk-+ |
+--->>----+-->>---| ->>-+
+-------+
因此,无论您有B1&
还是B2&
,您首先查找thunk,然后它会告诉您在哪里可以找到实际的基础子对象。这也解释了为什么不能从 A&
执行静态转换到任何派生类型:此信息在编译时根本不存在。
如需更深入的解释,请查看this fine article。 (在那个描述中,thunk 是 C
的 vtable 的一部分,并且虚拟继承总是需要维护 vtable,即使任何地方都没有虚拟 函数。)
【讨论】:
感谢您的出色回答。据我所知,thunk 是虚拟表的一部分。 IE。如果您不需要偏移量来获取对象功能正在运行,则不需要 thunk。如果您需要偏移量,那么在 vtable 的相应字段中有一个指向 thunk 的指针,其中包含偏移量和指向实际函数的指针。所以我很想知道,在我的示例中,vtables 的外观如何。 IE。它们指向哪些函数,哪些函数通过 thunk 指向。 同样,我很惊讶所有转换(静态、动态、重新解释)都将我转向了一个特定的函数 C::f。这很奇怪。你能解释一下(在这个例子中)他们每个人是如何工作的吗?另外,我读过很多关于这个的文章,你链接的文章是我读过的第一篇文章。它仍然无法帮助我理解这里发生了什么。 @user1544364 "所有类型转换 () 将我转到一个特定的函数" 不。这些类型转换返回一个对象指针,而不是一个函数。 @user1544364 "thunk,包含偏移量和指向实际函数的指针。" 不。thunk 不包含数据,thunk 由可执行代码组成。 thunk 只是一个优化的函数。 “精品文章”的链接坏了,但我找到了备份:cs.nyu.edu/courses/fall16/CSCI-UA.0470-001/slides/…【参考方案2】:我已经把你的代码拉了一点如下:
#include <stdio.h>
#include <stdint.h>
struct A
int a;
A() : a(32) f(0);
A(int i) : a(32) f(i);
virtual void f(int i) printf("%d\n", i);
;
struct B1 : virtual A
int b1;
B1(int i) : A(i), b1(33) f(i);
virtual void f(int i) printf("%d\n", i+10);
;
struct B2 : virtual A
int b2;
B2(int i) : A(i), b2(34) f(i);
virtual void f(int i) printf("%d\n", i+20);
;
struct C : B1, virtual B2
int c;
C() : B1(6),B2(3),A(1), c(35)
virtual void f(int i) printf("%d\n", i+30);
;
int main()
C foo;
intptr_t address = (intptr_t)&foo;
printf("offset A = %ld, sizeof A = %ld\n", (intptr_t)(A*)&foo - address, sizeof(A));
printf("offset B1 = %ld, sizeof B1 = %ld\n", (intptr_t)(B1*)&foo - address, sizeof(B1));
printf("offset B2 = %ld, sizeof B2 = %ld\n", (intptr_t)(B2*)&foo - address, sizeof(B2));
printf("offset C = %ld, sizeof C = %ld\n", (intptr_t)(C*)&foo - address, sizeof(C));
unsigned char* data = (unsigned char*)address;
for(int offset = 0; offset < sizeof(C); offset++)
if(!(offset & 7)) printf("| ");
printf("%02x ", (int)data[offset]);
printf("\n");
如您所见,这会打印出相当多的附加信息,使我们能够推断出内存布局。我机器上的输出(64 位 linux,小端字节序)是这样的:
1
23
16
offset A = 16, sizeof A = 16
offset B1 = 0, sizeof B1 = 32
offset B2 = 32, sizeof B2 = 32
offset C = 0, sizeof C = 48
| 00 0d 40 00 00 00 00 00 | 21 00 00 00 23 00 00 00 | 20 0d 40 00 00 00 00 00 | 20 00 00 00 00 00 00 00 | 48 0d 40 00 00 00 00 00 | 22 00 00 00 00 00 00 00
所以,我们可以这样描述布局:
+--------+----+----+--------+----+----+--------+----+----+
| vptr | b1 | c | vptr | a | xx | vptr | b2 | xx |
+--------+----+----+--------+----+----+--------+----+----+
这里,xx 表示填充。请注意编译器如何将变量 c
放入其非虚拟基的填充中。另请注意,所有三个 v 指针都是不同的,这允许程序推断出所有虚拟碱基的正确位置。
【讨论】:
以上是关于多重虚拟继承中的虚拟表和内存布局的主要内容,如果未能解决你的问题,请参考以下文章