10

最近のインタビューで、仮想関数と多重継承を伴うオブジェクト レイアウトについて尋ねられました。
多重継承を伴わずに実装する方法 (つまり、コンパイラが仮想テーブルを生成する方法、各オブジェクトに仮想テーブルへの秘密のポインターを挿入する方法など) のコンテキストで説明しました。
私の説明には何かが欠けているように思えました。
ここに質問があります(以下の例を参照)

  1. クラス C のオブジェクトの正確なメモリ レイアウトを教えてください。
  2. クラス C の仮想テーブル エントリ。
  3. クラス A、B、および C のオブジェクトのサイズ (sizeof によって返される) (8、8、16 ??)
  4. 仮想継承を使用するとどうなりますか。確かに、サイズと仮想テーブルのエントリが影響を受けるはずですか?

コード例:

class A {  
  public:   
    virtual int funA();     
  private:  
    int a;  
};

class B {  
  public:  
    virtual int funB();  
  private:  
    int b;  
};  

class C : public A, public B {  
  private:  
    int c;  
};   

ありがとう!

4

4 に答える 4

14

メモリレイアウトとvtableレイアウトは、コンパイラによって異なります。たとえば、私のgccを使用すると、次のようになります。

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

sizeof(int)とvtableポインターに必要なスペースも、コンパイラーごと、プラットフォームごとに異なる可能性があることに注意してください。sizeof(C)== 20で16ではない理由は、gccがAサブオブジェクトに8バイト、Bサブオブジェクトに8バイト、そのメンバーに4バイトを与えるためですint c

CのVtable
C :: _ ZTV1C:6uエントリ
0(int(*)(...))0
4(int(*)(...))(&_ ZTI1C)
8 A :: funA
12(int(*)(...))-0x00000000000000008
16(int(*)(...))(&_ ZTI1C)
20 B :: funB

クラスC
   サイズ=20整列=4
   ベースサイズ=20ベースアライン=4
C(0x40bd5e00)0
    vptr =((&C :: _ ZTV1C)+ 8u)
  A(0x40bd6080)0
      プライマリ-Cの場合(0x40bd5e00)
  B(0x40bd60c0)8
      vptr =((&C :: _ ZTV1C)+ 20u)

仮想継承の使用

class C : public virtual A, public virtual B

レイアウトがに変わります

CのVtable
C :: _ ZTV1C:12uエントリ
016u
4 8u
8(int(*)(...))0
12(int(*)(...))(&_ ZTI1C)
16 0u
20(int(*)(...))-0x00000000000000008
24(int(*)(...))(&_ ZTI1C)
28 A :: funA
32 0u
36(int(*)(...))-0x00000000000000010
40(int(*)(...))(&_ ZTI1C)
44 B :: funB

CのVTT
C :: _ ZTT1C:3uエントリ
0((&C :: _ ZTV1C)+ 16u)
4((&C :: _ ZTV1C)+ 28u)
8((&C :: _ ZTV1C)+ 44u)

クラスC
   サイズ=24整列=4
   ベースサイズ=8ベースアライン=4
C(0x40bd5e00)0
    vptridx = 0u vptr =((&C :: _ ZTV1C)+ 16u)
  A(0x40bd6080)8仮想
      vptridx = 4u vbaseoffset = -0x0000000000000000c vptr =((&C :: _ ZTV1C)+ 28u)
  B(0x40bd60c0)16仮想
      vptridx = 8u vbaseoffset = -0x00000000000000010 vptr =((&C :: _ ZTV1C)+ 44u)

gccを使用-fdump-class-hierarchyして、この情報を取得するために追加できます。

于 2009-08-24T08:40:36.363 に答える
5

多重継承で予期されることの 1 つは、(通常は最初ではない) サブクラスにキャストするときにポインターが変更される可能性があることです。インタビューの質問をデバッグして回答する際に知っておくべきこと。

于 2009-08-24T09:20:16.720 に答える
1

アラインメントまたはパディングビットについて言及せずに、この回答を完全な回答と見なす方法がわかりません。

アライメントの背景を少し説明しましょう。

「メモリ アドレス a は、a が n バイトの倍数 (n は 2 のべき乗) である場合、n バイト アラインされていると言われます。このコンテキストでは、バイトはメモリ アクセスの最小単位です。つまり、各メモリ アドレスは指定します。 n バイトにアラインされたアドレスは、2 進数で表現すると、log2(n) 個の最下位ゼロを持ちます。

b-bit 整列という別の言い回しは、ab/8 バイト整列アドレスを示します (例: 64 ビット整列は 8 バイト整列です)。

アクセスされるデータの長さが n バイトであり、データム アドレスが n バイトでアラインされている場合、メモリ アクセスはアラインされていると言われます。メモリー・アクセスがアライメントされていない場合、ミスアライメントと呼ばれます。定義により、バイト メモリ アクセスは常に整列されることに注意してください。

長さが n バイトのプリミティブ データを参照するメモリ ポインターは、n バイト アラインされたアドレスのみを含むことが許可されている場合、アラインされていると言われます。それ以外の場合は、アラインされていないと言われます。データ集合 (データ構造または配列) を参照するメモリ ポインターは、集合内の各プリミティブ データが整列されている場合にのみ整列されます。

上記の定義は、各プリミティブ データが 2 バイトの累乗長であると想定していることに注意してください。これが当てはまらない場合 (x86 の 80 ビット浮動小数点の場合のように)、コンテキストは、データが位置合わせされていると見なされるかどうかの条件に影響します。

データ構造は、制限付きと呼ばれる静的サイズのスタックまたは無制限と呼ばれる動的サイズのヒープのメモリに格納できます。" - Wiki から...

アラインメントを維持するために、コンパイラは構造体/クラス オブジェクトのコンパイル済みコードにパディング ビットを挿入します。" コンパイラ (またはインタプリタ) は通常、個々のデータ項目をアラインされた境界に割り当てますが、データ構造には異なるアラインメント要件を持つメンバーが含まれることがよくあります。適切なアラインメントを維持するために、トランスレータは通常、追加の名前のないデータ メンバーを挿入して、各メンバーが適切にアラインされるようにします。さらに、全体としてのデータ構造は、最終的な名前のないメンバーでパディングされる可能性があります. これにより、構造体の配列の各メンバーを適切に整列させることができます. .... ....

パディングは、構造体メンバーの後に、より大きなアライメント要件を持つメンバーが続く場合、または構造体の最後にのみ挿入されます" - Wiki

GCC がどのようにそれを行うかについての詳細は、以下を参照してください。

http://www.delorie.com/gnu/docs/gcc/gccint_111.html

テキスト「basic-align」を検索します

さて、この問題に行きましょう:

サンプル クラスを使用して、64 ビット Ubuntu で実行される GCC コンパイラ用にこのプログラムを作成しました。

int main() {
    cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
    A objA;
    C objC;
    cout<<__alignof__(objA.a)<<endl;
    cout<<sizeof(void*)<<endl;
    cout<<sizeof(int)<<endl;
    cout<<sizeof(A)<<endl;
    cout<<sizeof(B)<<endl;
    cout<<sizeof(C)<<endl;
    cout<<__alignof__(objC.a)<<endl;
    cout<<__alignof__(A)<<endl;
    cout<<__alignof__(C)<<endl;
    return 0;
}

そして、このプログラムの結果は次のとおりです。

4
8
4
16
16
32
4
8
8

では、説明しましょう。A と B の両方に仮想関数があるため、別々の VTABLE が作成され、それぞれのオブジェクトの先頭に VPTR が追加されます。

したがって、クラス A のオブジェクトには VPTR (A の VTABLE を指す) と int があります。ポインターの長さは 8 バイト、int の長さは 4 バイトになります。したがって、コンパイル前のサイズは 12 バイトです。ただし、コンパイラは int a の末尾に追加の 4 バイトをパディング ビットとして追加します。したがって、コンパイル後、A のオブジェクト サイズは 12+4 = 16 になります。

クラス B のオブジェクトについても同様です。

C のオブジェクトには 2 つの VPTR (クラス A とクラス B ごとに 1 つ) と 3 つの int (a、b、c) があります。したがって、サイズは 8 (VPTR A) + 4 (int a) + 4 (パディング バイト) + 8 (VPTR B) + 4 (int b) + 4 (int c) = 32 バイトになるはずです。したがって、C の合計サイズは 32 バイトになります。

于 2015-07-20T05:09:03.680 に答える