考虑以下层次结构:
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
?
最佳答案
虚拟基地与普通基地有很大的不同。请记住,“虚拟”意味着“在运行时确定”——因此整个基本子对象 必须在运行时确定。
假设您正在获取一个 B & x
引用,并且您的任务是找到 A::a
成员。如果继承是真实的,那么 B
有一个父类(super class) A
,因此您正在通过 x< 查看的
有一个 B
对象A
子对象,您可以在其中找到您的成员 A::a
。如果 x
的最派生对象有多个 A
类型的基类,那么您只能看到作为 B
子对象的特定拷贝.
但是如果继承是虚拟的,那么这一切都没有意义。我们不知道我们需要哪个 A
-subobject——这个信息在编译时根本不存在。我们可以处理一个实际的 B
对象,如 B y; B & x = y;
,或使用 C
对象,如 C z; B & x = z;
,或者从 A
多次派生的完全不同的东西。唯一知道的方法是在运行时 A
找到实际的基数。
这可以通过多一层运行时间接实现。 (请注意,与非虚拟函数相比,这与虚拟 函数 是如何通过额外一级的运行时间接实现的方式完全平行的。)一个解决方案不是使用指向 vtable 或基本子对象的指针,而是存储指向实际基子对象的指针。这有时被称为“thunk”或“trampoline”。
所以实际对象 C z;
可能如下所示。内存中的实际排序取决于编译器并且并不重要,我已经抑制了 vtables。
+-+------++-+------++-----++-----+
|T| B1 ||T| B2 || C || A |
+-+------++-+------++-----++-----+
| | |
V V ^
| | +-Thunk-+ |
+--->>----+-->>---| ->>-+
+-------+
因此,无论您有 B1&
还是 B2&
,您首先要查找 thunk,thunk 会告诉您在哪里可以找到实际的 base子对象。这也解释了为什么您不能执行从 A&
到任何派生类型的静态转换:此信息在编译时根本不存在。
要获得更深入的解释,请查看 this fine article . (在该描述中,thunk 是 C
的 vtable 的一部分,虚拟继承总是需要维护 vtables,即使任何地方都没有虚拟函数。)
关于c++ - 多重虚拟继承中的虚拟表和内存布局,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30891915/