虚拟表和内存布局在多个虚拟inheritance中
考虑以下层次结构:
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
实例的确切内存布局是什么? 它包含多less个vptrs,每一个都放在哪里? 哪个虚拟表与C的虚拟表共享? 每个虚拟表包含什么?这里我怎么理解布局:
---------------------------------------------------------------- |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a | ----------------------------------------------------------------
其中
AptrOfBx
是指向Bx
包含A
实例的指针(因为inheritance是虚拟的)。
那是对的吗?vptr1
指向哪个函数?vptr2
指向哪个函数? -
给出以下代码
C* c = new C(); dynamic_cast<B1*>(c)->f(3); static_cast<B2*>(c)->f(3); reinterpret_cast<B2*>(c)->f(3);
为什么所有的电话打印
33
?
虚拟基地与普通基地非常不同。 请记住,“虚拟”是指“在运行时确定” – 因此必须在运行时确定整个基础子对象 。
想象一下,你正在得到一个B & x
参考,你的任务是findA::a
成员。 如果inheritance是真实的,那么B
有一个超类A
,因此你通过x
查看的B
有一个A
sububject,你可以在其中find你的成员A::a
。 如果x
的派生最多的对象有多个typesA
,那么只能看到B
的子对象的特定副本。
但是,如果inheritance是虚拟的,这没有任何意义。 我们不知道我们需要哪个 A
-subobject – 这个信息在编译时根本不存在 。 我们可以像在B y; B & x = y;
一样处理一个实际的B
B y; B & x = y;
B y; B & x = y;
,或者像C z; B & x = z;
这样的C z; B & x = z;
C z; B & x = z;
,或者完全不同的东西,从A
中衍生出来很多次。 要知道的唯一方法是在运行时find实际的基数A
这可以通过一个更多级别的运行时间间接来实现。 (请注意,与非虚函数相比,这是如何与一个额外级别的运行时间间接方式实现的虚拟函数完全平行的。)一种解决scheme是存储一个指向指针的指针 ,而不是指向vtable或base子对象的指针到实际的基础子对象。 这有时被称为“thunk”或“蹦床”。
所以实际的对象C z;
可能看起来如下。 内存中的实际顺序取决于编译器和不重要的,我压制了vtable。
+-+------++-+------++-----++-----+ |T| B1 ||T| B2 || C || A | +-+------++-+------++-----++-----+ | | | VV ^ | | +-Thunk-+ | +--->>----+-->>---| ->>-+ +-------+
因此,不pipe你是否有B1&
或B2&
,你首先查看thunk,然后这个人告诉你在哪里find实际的基础子对象。 这也解释了为什么你不能从A&
任何派生types执行静态转换:这个信息在编译时根本不存在。
有关更深入的解释,请看这篇精美的文章 。 (在这个描述中,thunk是C
的vtable的一部分,虚拟inheritance总是需要vtables的维护,即使在任何地方都没有虚函数 )。
我把你的代码做了一下,如下所示:
#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表示填充。 注意编译器如何将variablesc
放入其非虚拟基的填充中。 还要注意的是,所有三个v指针是不同的,这允许程序推导出所有虚拟基地的正确位置。