考虑以下层次结构:
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
实例的完全内存布局是什么?它包含多少个vptrs,它们中的每一个都放在哪里?哪个虚拟表与C的虚拟表共享?每个虚拟表包含什么?
我在这里理解布局:
----------------------------------------------------------------
|vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a |
----------------------------------------------------------------
其中AptrOfBx
是A
包含Bx
实例的指针(因为继承是虚拟的)。
那是对的吗? 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
?
答案 0 :(得分:16)
虚拟基地与普通基地有很大不同。请记住,“虚拟”表示“在运行时确定” - 因此必须在运行时确定整个基础子对象。
想象一下,您获得了B & x
引用,并且您的任务是找到A::a
成员。如果继承是真实的,那么B
有一个超类A
,因此您通过B
查看的x
- 对象有一个A
- 子对象您可以在其中找到您的成员A::a
。如果x
的派生程度最高的对象具有多个A
类型的基础,那么您只能看到该B
的子对象的特定副本。
但如果继承是虚拟的,那么这一切都没有意义。我们不知道我们需要哪个 A
- 子对象 - 这些信息在编译时根本不存在 。我们可以处理实际的B
- 对象,如B y; B & x = y;
,或C
- 对象,如C z; B & x = z;
,或完全不同的对象,实际上来自{{1}多次。要知道的唯一方法是在运行时找到实际的基础A
。
这可以通过一个更高级别的运行时间接来实现。 (注意这与如何使用一个额外级别的运行时间接实现虚拟函数与非虚函数相比完全平行。)而不是指向vtable或基础子对象的指针,一个解决方案是将指针存储到指针到实际的基础子对象。这有时被称为“thunk”或“trampoline”。
因此实际对象A
可能如下所示。内存中的实际排序由编译器决定并且不重要,我已经压制了vtable。
C z;
因此,无论您是+-+------++-+------++-----++-----+
|T| B1 ||T| B2 || C || A |
+-+------++-+------++-----++-----+
| | |
V V ^
| | +-Thunk-+ |
+--->>----+-->>---| ->>-+
+-------+
还是B1&
,您首先要查找thunk,然后轮流会告诉您在哪里找到实际的基础子对象。这也解释了为什么你不能执行从B2&
到任何派生类型的静态转换:这些信息在编译时根本就不存在。
要获得更深入的解释,请查看this fine article。 (在该描述中,thunk是A&
的vtable的一部分,虚拟继承总是需要维护vtable,即使在任何地方都没有虚拟函数。)
答案 1 :(得分:3)
我对你的代码进行了如下操作:
#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指针都不同,这允许程序推导出所有虚拟基础的正确位置。