C++ 多重虚拟继承中的虚拟表和内存布局

C++ 多重虚拟继承中的虚拟表和内存布局,c++,multiple-inheritance,vtable,virtual-inheritance,memory-layout,C++,Multiple Inheritance,Vtable,Virtual Inheritance,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 : v

考虑以下层次结构:

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; }
};
其中,
AptrOfBx
是指向
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);
    
    C*C=newc();
    动态铸造(c)->f(3);
    静态铸造(c)->f(3);
    重新解释铸造(c)->f(3);
    
    为什么要调用
    f
    print
    33


  • 虚拟基地与普通基地大不相同。请记住,“virtual”意味着“在运行时确定”——因此,整个基本子对象必须在运行时确定

    假设您正在获得一个
    B&x
    引用,您的任务是查找
    a::a
    成员。如果继承是真实的,那么
    B
    有一个超类
    a
    ,因此通过
    x
    查看的
    B
    -对象有一个
    a
    -子对象,您可以在其中找到成员
    a::a
    。如果
    x
    的最派生对象有多个
    A
    类型的基,则只能看到作为
    B
    子对象的特定副本

    但是如果继承是虚拟的,那么这些都没有意义。我们不知道我们需要哪个
    子对象,这些信息在编译时根本不存在。我们可以处理实际的
    B
    -对象,如
    by;B&x=y
    ,或使用类似
    cz的
    C
    -对象;B&x=z,或者是从
    A
    中多次衍生出来的完全不同的东西。知道的唯一方法是在运行时找到实际的基
    A

    这可以通过一个更高级别的运行时间接寻址来实现。(请注意,与非虚拟函数相比,虚拟函数是如何通过一个额外的运行时间接寻址级别来实现的。)一种解决方案是存储指向实际基子对象的指针,而不是使用指向vtable或base子对象的指针。这有时被称为“扑通”或“蹦床”

    因此,实际对象
    cz可能如下所示。内存中的实际顺序由编译器决定,并不重要,我已经抑制了vtables

    +-+------++-+------++-----++-----+
    |T|  B1  ||T|  B2  ||  C  ||  A  |
    +-+------++-+------++-----++-----+
     |         |                 |
     V         V                 ^
     |         |       +-Thunk-+ |
     +--->>----+-->>---|     ->>-+
                       +-------+
    
    因此,无论您是使用
    B1&
    还是
    B2&
    ,您都需要首先查找thunk,然后该thunk会告诉您在哪里可以找到实际的基本子对象。这也解释了为什么不能执行从
    a&
    到任何派生类型的静态强制转换:编译时根本不存在这些信息


    要获得更深入的解释,请参阅。(在该描述中,thunk是
    C
    的vtable的一部分,虚拟继承总是需要维护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");
    }
    
    +--------+----+----+--------+----+----+--------+----+----+
    |  vptr  | b1 | c  |  vptr  | a  | xx |  vptr  | b2 | xx |
    +--------+----+----+--------+----+----+--------+----+----+
    
    因此,我们可以将布局描述如下:

    #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");
    }
    
    +--------+----+----+--------+----+----+--------+----+----+
    |  vptr  | b1 | c  |  vptr  | a  | xx |  vptr  | b2 | xx |
    +--------+----+----+--------+----+----+--------+----+----+
    

    这里,xx表示填充。注意编译器是如何将变量
    c
    放入其非虚拟基的填充中的。还要注意的是,所有三个v型指针都是不同的,这允许程序推断出所有虚基的正确位置。

    这是家庭作业还是好奇心?实际上这是考试。但是我确信,如果我最终理解了这个例子中的工作原理,我就能理解与多重继承和虚拟继承相关的任何内容;intptr_t offsetB1=(intptr_t)(B1*)和foo-(intptr_t)和foo,其他基的起始可以类似地导出。另外,计算所有类的
    sizeof
    应该会给你另一个好的线索。谢谢你的回答。据我所知,thunk是虚拟表的一部分。也就是说,如果不需要偏移量来获取对象函数正在处理的对象,则不需要砰的一声。如果需要偏移量,那么在vtable的相应字段中有一个指向thunk的指针,其中包含偏移量和指向实际函数的指针。所以我很想知道,在我的示例中vtables看起来是怎样的。也就是说,它们指向哪一个函数,哪一个函数是通过thunks指向的。同样,我非常惊讶的是,所有的强制转换(静态、动态、重新解释)都将我变成了一个特定的函数C::f。这很奇怪。你能解释一下(在这个例子中)它们是如何工作的吗?另外,我读过很多关于这方面的文章,你链接的那篇文章是我读过的第一篇文章之一。它仍然不能帮助我理解这里发生的事情。@user1544364“所有的强制转换()将我转换为一个特定的函数”否。这些强制转换返回一个对象指针,而不是函数。@user1544364“thunk,它包含偏移量和指向实际函数的指针。”否。thunk不包含数据,thunk由可执行代码组成。thunk只是一个优化功能。指向“精品”的链接已断开,但我找到了备份: