多个虚拟继承中的虚拟表和内存布局

JeB*_*JeB 13 c++ multiple-inheritance virtual-inheritance vtable memory-layout

考虑以下层次结构

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; }
};
Run Code Online (Sandbox Code Playgroud)
  1. 实例的确切内存布局是C什么?它包含多少个vptrs,它们中的每一个都放在哪里?哪个虚拟表与C的虚拟表共享?每个虚拟表包含什么?

    在这里我如何理解布局:

    ----------------------------------------------------------------
    |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a |
    ----------------------------------------------------------------
    
    Run Code Online (Sandbox Code Playgroud)

    其中AptrOfBx是指针A实例Bx包含(因为继承是虚拟的).
    那是对的吗?哪些功能vptr1指向?哪些功能vptr2指向?

  2. 给出以下代码

    C* c = new C();
    dynamic_cast<B1*>(c)->f(3);
    static_cast<B2*>(c)->f(3);
    reinterpret_cast<B2*>(c)->f(3);
    
    Run Code Online (Sandbox Code Playgroud)

    为什么要f打印所有电话33

Ker*_* SB 17

虚拟基地与普通基地有很大不同.请记住,"虚拟"意味着"在运行时确定" - 因此必须在运行时确定整个基础子对象.

想象一下,您正在获得B & x参考,并且您的任务是找到该A::a成员.如果继承是真实的,那么B有一个超类A,因此B你正在查看的对象x有一个A-subobject,你可以在其中找到你的成员A::a.如果最派生的对象x具有多个类型的基础A,那么您只能看到该子对象的特定副本B.

但如果继承是虚拟的,那么这一切都没有意义.我们不知道我们需要哪个子 A对象 - 这些信息在编译时根本不存在.我们可以用一个实际的要处理B-object中B y; B & x = y;,或用C-object一样C z; B & x = z;,从几乎派生,或完全不同的东西A很多倍.唯一知道的方法是A 在运行时找到实际的基数.

这可以通过一个更高级别的运行时间接来实现.(请注意,与非虚函数相比,这与一个额外级别的运行时间接实现如何实现虚函数完全并行.)一种解决方案是存储指向指针的指针,而不是指向vtable或基本子对象的指针.到实际的基础子对象.这有时被称为"thunk"或"trampoline".

所以实际对象C z;可能如下所示.内存中的实际排序取决于编译器并且不重要,我已经抑制了vtable.

+-+------++-+------++-----++-----+
|T|  B1  ||T|  B2  ||  C  ||  A  |
+-+------++-+------++-----++-----+
 |         |                 |
 V         V                 ^
 |         |       +-Thunk-+ |
 +--->>----+-->>---|     ->>-+
                   +-------+
Run Code Online (Sandbox Code Playgroud)

因此,无论你是否拥有a B1&或a B2&,你首先要查看thunk,然后那个反过来告诉你在哪里找到实际的基础子对象.这也解释了为什么不能从A&任何派生类型执行静态转换:这些信息在编译时根本不存在.

有关更深入的解释,请查看此精美文章.(在该描述中,thunk是vtable的一部分C,虚拟继承总是需要维护vtable,即使在任何地方都没有虚函数.)