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はいくつあり、それぞれが正確に配置されていますか?Cの仮想テーブルと共有されている仮想テーブルはどれですか?各仮想テーブルには正確に何が含まれていますか?

    ここで私がレイアウトをどのように理解するか:

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

    ここで、はを含むインスタンスAptrOfBxへのポインタです(継承は仮想であるため)。 あれは正しいですか?どの機能が指しているのですか?どの機能が指しているのですか?ABx
    vptr1vptr2

  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表示しxているA-objectには、メンバーを見つけることができる-subobjectがありますA::a。の最も派生したオブジェクトにxタイプの複数のベースがあるA場合、のサブオブジェクトである特定のコピーのみを表示できますB

しかし、継承が仮想である場合、これは意味がありません。どの Aサブオブジェクトが必要かわかりません。この情報は、コンパイル時に存在しません。のように実際のオブジェクトを処理することも、のようなオブジェクトを処理することもできますBB y; B & x = y;あるいは、事実上何回も派生Cするま​​ったく異なるものを処理することもできます。知る唯一の方法は、実行時に実際のベースを見つけることです。C z; B & x = z;AA

これは、もう1つのレベルのランタイム間接参照で実装できます。(これは、非仮想関数と比較して、実行時間接参照の1つの追加レベルで仮想関数を実装する方法と完全に並行していることに注意してください。)vtableまたはbaseサブオブジェクトへのポインターを使用する代わりに、ポインターへのポインターを格納するのが1つの解決策です。実際のベースサブオブジェクトに。これは「サンク」または「トランポリン」と呼ばれることもあります。

したがって、実際のオブジェクトC z;は次のようになります。メモリ内の実際の順序はコンパイラ次第であり、重要ではありません。vtableを抑制しました。

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

B1&したがって、aまたはaがあるかどうかに関係なくB2&、最初にサンクを検索し、次にサンクが実際のベースサブオブジェクトの場所を示します。これは、から派生型への静的キャストを実行できない理由も説明していA&ます。この情報は、コンパイル時に存在しないだけです。

より詳細な説明については、このすばらしい記事をご覧ください。(その説明では、サンクはの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を非仮想ベースのパディングにどのように配置したかに注意してください。また、3つのvポインターはすべて異なるため、プログラムはすべての仮想ベースの正しい位置を推測できます。

于 2014-07-17T20:47:02.170 に答える