14

考虑以下层次结构:

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; }
};
  1. 实例的确切内存布局是什么?C它包含多少个 vptr,每个 vptr 的确切放置位置?哪些虚拟表与 C 的虚拟表共享?每个虚拟表究竟包含什么?

    这里我是如何理解布局的:

    ----------------------------------------------------------------
    |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a |
    ----------------------------------------------------------------
    

    whereAptrOfBx是指向包含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);
    

    为什么所有的电话都f打印33

4

2 回答 2

18

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

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

但是如果继承是虚拟的,那么这一切都没有意义。我们不知道我们需要哪个 -subobject——这些信息在编译时A根本不存在。我们可以处理B像 in 那样的实际 -object B y; B & x = y;,或者C像 那样处理 -object C z; B & x = z;,或者完全不同的东西,实际上是从A更多次衍生而来的。唯一知道的方法是A 在运行时找到实际的基础。

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

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

+-+------++-+------++-----++-----+
|T|  B1  ||T|  B2  ||  C  ||  A  |
+-+------++-+------++-----++-----+
 |         |                 |
 V         V                 ^
 |         |       +-Thunk-+ |
 +--->>----+-->>---|     ->>-+
                   +-------+

因此,无论您有 aB1&还是 a B2&,您首先查找 thunk,然后该 thunk 会告诉您在哪里可以找到实际的基础子对象。这也解释了为什么您不能执行从 anA&到任何派生类型的静态转换:此信息在编译时根本不存在。

如需更深入的解释,请查看这篇精美的文章。(在那个描述中,thunk 是 vtable 的一部分C,并且虚拟继承总是需要维护 vtable,即使任何地方都没有虚拟功能。)

于 2012-07-22T21:28:46.290 回答
4

我把你的代码拉了一点如下:

#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 指针都是不同的,这允许程序推断所有虚拟碱基的正确位置。

于 2014-07-17T20:47:02.170 回答