10

vptr とメモリ内のオブジェクトの表現について少し混乱しています。問題をよりよく理解するのに役立つことを願っています。

  1. Binherits fromAと、両方が virtual functions を定義することを検討してくださいf()。私が学んだことから、メモリ内のクラス B のオブジェクトの表現は次のように[ vptr | A | B ] なります。また、オブジェクトをからにキャストしても、オブジェクトの最後の部分を無視する以外は何もしないことも理解しました。本当ですか?この振る舞いは間違っていませんか?型のオブジェクトがではなく メソッドを実行するようにします。vtblvptrB::f()BABAA::f()B::f()

  2. クラスの数vtablesとしてシステムにいくつかありますか?

  3. vtable2 つ以上のクラスを継承する of クラスはどのようになりますか? C のオブジェクトはメモリ上でどのように表現されるでしょうか?

  4. 質問 3 と同じですが、仮想継承があります。

4

3 に答える 3

16

以下は GCC に当てはまります (LLVM linkにも当てはまります) が、使用しているコンパイラにも当てはまります。これらはすべて実装依存であり、C++ 標準では管理されていません。ただし、GCC は独自のバイナリ標準ドキュメントItanium ABIを作成します。

C++ での仮想関数のパフォーマンスに関する私の記事の一部として、仮想テーブルがどのようにレイアウトされるかの基本的な概念をより簡単な言葉で説明しようとしました。質問に対する回答は次のとおりです。

  1. オブジェクトの内部表現を表すより正しい方法は次のとおりです。

    | vptr | ======= | ======= |  <-- your object
           |----A----|         |
           |---------B---------|
    

    B にはその基本クラス が含まれAており、終了後に自分のメンバーをいくつか追加するだけです。

    B*からへのキャストはA*実際には何もせず、同じポインターを返し、同じvptrままです。しかし、一言で言えば、仮想関数は常に vtable 経由で呼び出されるわけではありません。他の関数と同じように呼び出されることもあります。

    詳しい説明はこちら。メンバー関数を呼び出す 2 つの方法を区別する必要があります。

    A a, *aptr;
    a.func();         // the call to A::func() is precompiled!
    aptr->A::func();  // ditto
    aptr->func();     // calls virtual function through vtable.
                      // It may be a call to A::func() or B::func().
    

    問題は、関数がどのように呼び出されるかがコンパイル時にわかっていることです: vtable を介して、または単に通常の呼び出しになります。そして問題は、キャスト式の型はコンパイル時に認識されるため、コンパイラはコンパイル時に適切な関数を選択するということです。

    B b, *bptr;          
    static_cast<A>(b)::func(); //calls A::func, because the type
       // of static_cast<A>(b) is A!
    

    この場合、vtable の内部も検索しません。

  2. 一般的に、いいえ。クラスが複数のベースから継承し、それぞれが独自の vtable を持つ場合、クラスは複数の vtable を持つことができます。このような仮想テーブルのセットは、「仮想テーブル グループ」を形成します (pt. 3 を参照)。

    クラスには、複雑なオブジェクトのベースを構築するときに仮想関数を正しく分配するために、一連の構築 vtables も必要です。私がリンクした標準でさらに読むことができます。

  3. これが例です。がおよびCから継承し、各クラスが 、および、またはその名前に関連する仮想関数を定義していると仮定します。ABvirtual void func()abc

    にはC、2 つの vtable の vtable グループがあります。1 つの vtable を共有しA(現在のクラスの独自の関数が移動する vtable は「プライマリ」と呼ばれます)、vtableBが追加されます。

    | C::func()   |   a()  |  c()  || C::func()  |   b()   |
    |---- vtable for A ----|        |---- vtable for B ----| 
    |--- "primary virtual table" --||- "secondary vtable" -|
    |-------------- virtual table group for C -------------|
    

    メモリ内のオブジェクトの表現は、その vtable とほぼ同じように見えます。グループ内のすべての vtable の前に a を追加するvptrだけで、データがオブジェクト内でどのように配置されているかを大まかに見積もることができます。これについては、GCC バイナリ標準の関連セクションを参照してください。

  4. 仮想ベース (一部) は、vtable グループの最後に配置されます。これは、各クラスが仮想ベースを 1 つだけ持つ必要があり、それらが「通常の」vtable と混在している場合、コンパイラは構築された vtable の一部を再利用して派生クラスの vtable を作成できないためです。これにより、不要なオフセットが計算され、パフォーマンスが低下します。

    このような配置により、仮想ベースは vtables に次の追加要素も導入します:vcallオフセット (完全なオブジェクト内の仮想ベースへのポインターから仮想関数をオーバーライドするクラスの先頭にジャンプするときに、最終オーバーライダーのアドレスを取得するため)そこで定義された各仮想関数に対して。また、各仮想ベースはvbase、派生クラスの vtable に挿入されるオフセットを追加します。仮想ベースのデータがどこから始まるかを見つけることができます (実際のアドレスは階層に依存するため、プリコンパイルできません: 仮想ベースはオブジェクトの最後にあり、最初からのシフトは非仮想ベースの数によって異なります)。現在のクラスが継承するクラス)。

うわー、不必要な複雑さをあまり導入していないことを願っています。いずれにせよ、元の標準、または独自のコンパイラの任意のドキュメントを参照できます。

于 2010-07-24T11:42:49.340 に答える
2
  1. それは私には正しいようです。A ポインターを使用しているかのように、A が提供するものと、A vtable から利用可能な B 関数の実装のみが必要な場合は、間違っていません (コンパイラーと階層の複雑さに応じて、複数の vtable が存在する可能性があります)。
  2. はいと思いますが、コンパイラの実装に依存するため、実際に知る必要はありません。
  3. 4. さらに読む。

Multiple Inheritance Considered Usefulを読むことをお勧めします。これは長い記事ですが、C++ で継承がどのように機能するかを詳細に説明しているため、主題についてより明確になります (図のリンクは機能しませんが、ページの下部にあります)。 )。

于 2010-07-24T10:39:45.010 に答える
-1
  1. オブジェクト B が A から継承する場合、B のメモリ表現は次のようになります。

    • A の仮想テーブルへのポインタ
    • 特定の変数/関数
    • B の仮想テーブルへのポインタ
    • B 特定の変数/関数/オーバーライド

    B* b = new B(); がある場合 (A)b->f() の場合:

    • f が仮想関数として宣言されている場合、b は B 型であるため、B の実装が呼び出されます。
    • f が仮想関数として宣言されていない場合、呼び出されたときに、vtable で正しい実装を検索することはなく、A の実装が呼び出されます。
  2. すべてのオブジェクトには独自の vtable があります (調査する必要があるため、これを当然のことと考えないでください)

  3. 多重継承を扱うときの vtable レイアウトの例については、これを見てください。

  4. ダイヤモンドの継承と vtable 表現に関する議論については、これを参照してください

于 2010-07-24T11:50:13.460 に答える