29

仮想のオブジェクトサイズについていくつか質問があります。

1)仮想機能

class A {
    public:
       int a;
       virtual void v();
    }

クラスAのサイズは8バイトです....1つの整数(4バイト)と1つの仮想ポインタ(4バイト)それは明らかです!

class B: public A{
    public:
       int b;
       virtual void w();
}

クラスBのサイズはどれくらいですか?sizeof Bを使用してテストしましたが、12を出力します

クラスBとクラスAの両方に仮想機能がある場合でも、vptrが1つしかないということですか?vptrが1つしかないのはなぜですか?

class A {
public:
    int a;
    virtual void v();
};

class B {
public:
    int b;
    virtual void w();
};

class C :  public A, public B {
public:
    int c;
    virtual void x();
};

Cのサイズは20.......です。

この場合、2つのvptrがレイアウトに含まれているようです.....これはどのように発生しますか?2つのvptrは1つはクラスA用で、もう1つはクラスB用だと思います。クラスCの仮想関数用のvptrはありませんか?

私の質問は、継承のvptrの数についてのルールは何ですか?

2)仮想継承

    class A {
    public:
        int a;
        virtual void v();
    };

    class B: virtual public A{                  //virtual inheritance 
    public:
        int b;
        virtual void w();
    };

    class C :  public A {                      //non-virtual inheritance
    public:
        int c;
        virtual void x();
    };

class D: public B, public C {
public:
    int d;
    virtual void y();
};

Aのサイズは8バイトです--------------4(int a)+ 4(vptr)= 8

Bのサイズは16バイトです--------------仮想がない場合は4+4 + 4 = 12である必要があります。なぜここにさらに4バイトがあるのですか?クラスBのレイアウトは何ですか?

Cのサイズは12バイトです。-------------- 4 + 4 + 4 = 12.明らかです!

Dのサイズは32バイトです--------------16(クラスB)+ 12(クラスC)+ 4(int d)= 32である必要があります。そうですか?

    class A {
    public:
        int a;
        virtual void v();
    };

    class B: virtual public A{                       //virtual inheritance here
    public:
        int b;
        virtual void w();
    };

    class C :  virtual public A {                    //virtual inheritance here
    public:
        int c;
        virtual void x();
    };

  class D: public B, public C {
   public:
        int d;
        virtual void y();
    };

Aのサイズは8です

Bのサイズは16です

Cのsizeofは16です

sizeofDは28です28= 16(クラスB)+ 16(クラスC)-8(クラスA)+ 4(これは何ですか?)

私の質問は、仮想継承が適用されるときになぜ余分なスペースがあるのですか?

この場合のオブジェクトサイズの基本的なルールは何ですか?

仮想がすべての基本クラスと一部の基本クラスに適用される場合の違いは何ですか?

4

6 に答える 6

23

これはすべて実装定義です。VC10 Beta2 を使用しています。この内容 (仮想関数の実装) を理解するには、Visual Studio コンパイラの秘密のスイッチ/d1reportSingleClassLayoutXXXについて知っておく必要があります。すぐに説明します。

基本的なルールは、オブジェクトへのポインタのオフセット 0 に vtable を配置する必要があるということです。これは、多重継承のための複数の vtable を意味します。

ここでいくつか質問があります。一番上から始めます。

クラスBとクラスAの両方が仮想機能を持っていても、vptrは1つしかないということですか?vptr が 1 つしかないのはなぜですか?

これが仮想関数の仕組みです。基本クラスと派生クラスで同じ vtable ポインター (派生クラスの実装を指す) を共有する必要があります。

この場合、2 つの vptr がレイアウト内にあるようです.....これはどのように発生しますか? クラスA用とクラスB用の2つのvptrがあると思います....クラスCの仮想機能用のvptrはありませんか?

これは、/d1reportSingleClassLayoutC によって報告されたクラス C のレイアウトです。

class C size(20):
        +---
        | +--- (base class A)
 0      | | {vfptr}
 4      | | a
        | +---
        | +--- (base class B)
 8      | | {vfptr}
12      | | b
        | +---
16      | c
        +---

おっしゃるとおり、基本クラスごとに 1 つずつ、合計 2 つの vtable があります。これが多重継承での動作です。C* が B* にキャストされた場合、ポインター値は 8 バイト調整されます。仮想関数呼び出しが機能するには、vtable がオフセット 0 にある必要があります。

クラス A の上記のレイアウトの vtable は、クラス C の vtable として扱われます (C* を介して呼び出された場合)。

B のサイズは 16 バイトです -------------- virtual がなければ、4 + 4 + 4 = 12 になります。なぜ、ここにさらに 4 バイトがあるのでしょうか? クラスBのレイアウトは?

これは、この例のクラス B のレイアウトです。

class B size(20):
        +---
 0      | {vfptr}
 4      | {vbptr}
 8      | b
        +---
        +--- (virtual base A)
12      | {vfptr}
16      | a
        +---

ご覧のとおり、仮想継承を処理するための追加のポインターがあります。仮想継承は複雑です。

D のサイズは 32 バイト -------------- 16(class B) + 12(class C) + 4(int d) = 32 である必要があります。

いいえ、36 バイトです。仮想継承と同じ扱い。この例の D のレイアウト:

class D size(36):
        +---
        | +--- (base class B)
 0      | | {vfptr}
 4      | | {vbptr}
 8      | | b
        | +---
        | +--- (base class C)
        | | +--- (base class A)
12      | | | {vfptr}
16      | | | a
        | | +---
20      | | c
        | +---
24      | d
        +---
        +--- (virtual base A)
28      | {vfptr}
32      | a
        +---

私の質問は、仮想継承が適用されたときに余分なスペースがあるのはなぜですか?

仮想基底クラス ポインター、複雑です。基本クラスは、仮想継承で「結合」されます。基本クラスをクラスに埋め込む代わりに、クラスはレイアウト内の基本クラス オブジェクトへのポインターを持ちます。仮想継承 (「ダイヤモンド」クラス階層) を使用する 2 つの基底クラスがある場合、それらは両方とも、その基底クラスの個別のコピーを持つのではなく、オブジェクト内の同じ仮想基底クラスを指します。

この場合のオブジェクト サイズの基本ルールは何ですか?

大事なポイント; ルールはありません。コンパイラは必要なことを何でも実行できます。

そして最後の詳細。私がコンパイルしているこれらすべてのクラスレイアウト図を作成するには:

cl test.cpp /d1reportSingleClassLayoutXXX

XXX は、レイアウトを表示する構造体/クラスの部分文字列の一致です。これを使用して、さまざまな継承スキームの影響、およびパディングが追加される理由/場所などを自分で調べることができます。

于 2010-01-10T21:54:25.957 に答える
3

それについて考える良い方法は、アップキャストを処理するために何をしなければならないかを理解することです。あなたが説明したクラスのオブジェクトのメモリレイアウトを示すことで、あなたの質問に答えようとします。

コードサンプル #2

メモリ レイアウトは次のとおりです。

vptr | A::a | B::b

B へのポインターをタイプ A にアップキャストすると、同じ vptr が使用された同じアドレスになります。これが、ここで vptr を追加する必要がない理由です。

コードサンプル #3

vptr | A::a | vptr | B::b | C::c

ご覧のとおり、ご想像のとおり、ここには 2 つの vptr があります。なんで?C から A にアップキャストする場合、アドレスを変更する必要がないため、同じ vptr を使用できるのは事実です。しかし、C から B にアップキャストする場合は、その変更必要であり、それに対応して、結果のオブジェクトの先頭に vptr が必要です。

そのため、最初のクラスを超えて継承されたクラスには、追加の vptr が必要になります (継承されたクラスに仮想メソッドがない場合を除きます。その場合、vptr はありません)。

コードサンプル #4 以降

仮想的に派生させる場合、派生クラスのメモリ レイアウト内の場所を指すために、ベース ポインターと呼ばれる新しいポインターが必要です。もちろん、複数のベース ポインターが存在する可能性があります。

では、メモリ レイアウトはどのように見えるでしょうか。それはコンパイラに依存します。あなたのコンパイラでは、おそらく次のようなものです

vptr | ベースポインタ | B::b | vptr | A::a | C::c | vptr | あ::あ
          \-----------------------------------------^

しかし、他のコンパイラーは、仮想テーブルにベースポインターを組み込む場合があります (オフセットを使用することにより、別の質問に値します)。

仮想的な方法で派生する場合、派生クラスはメモリ レイアウトに 1 回だけ表示されるため、ベース ポインターが必要です (例のように、通常どおりに派生している場合はさらに表示される可能性があります)。まったく同じ場所。

編集: 明確化 - すべては実際にはコンパイラに依存します。私が示したメモリ レイアウトは、コンパイラによって異なる場合があります。

于 2010-01-10T22:00:41.527 に答える
3

引用> 私の質問は、継承における vptr の数に関するルールは何ですか?

ルールはありません。すべてのコンパイラ ベンダーは、継承のセマンティクスを自分が適切と考える方法で実装することが許可されています。

クラス B: public A {}、サイズ = 12。これはごく普通のことです。両方の仮想メソッドを持つ B の 1 つの vtable、vtable ポインター + 2*int = 12

class C : public A, public B {}, size = 20. C は A または B のいずれかの vtable を任意に拡張できます. 2*vtable ポインタ + 3*int = 20

仮想継承: これは、文書化されていない動作の端に実際にぶつかる場所です。たとえば、MSVC では #pragma vtordisp および /vd コンパイル オプションが適切になります。この記事にはいくつかの背景情報があります。これを数回調べた結果、コンパイル オプションの頭字語は、これを使用した場合にコードに何が起こるかを表すものであると判断しました。

于 2010-01-10T22:12:28.510 に答える
2

これはすべて、完全に定義された実装です。あなたはそれを当てにすることはできません。「ルール」はありません。

継承の例では、クラス A と B の仮想テーブルは次のようになります。

      class A
+-----------------+
| pointer to A::v |
+-----------------+

      class B
+-----------------+
| pointer to A::v |
+-----------------+
| pointer to B::w |
+-----------------+

ご覧のとおり、クラス B の仮想テーブルへのポインタがあれば、クラス A の仮想テーブルとしても完全に有効です。

クラス C の例では、考えてみると、クラス C、クラス A、およびクラス B のテーブルとして両方とも有効な仮想テーブルを作成する方法はありません。したがって、コンパイラは 2 つを作成します。1 つの仮想テーブルはクラス A および C (ほとんどの場合) に有効であり、もう 1 つはクラス A および B に有効です。

于 2010-01-10T21:52:17.267 に答える
1

これは明らかにコンパイラの実装に依存します。とにかく、以下にリンクされた古典的な論文で与えられた実装から次のルールを要約できると思います。これにより、例で得られるバイト数が得られます(32ではなく36バイトになるクラスDを除く!!!) :

クラス T のオブジェクトのサイズは次のとおりです。

  • そのフィールドのサイズに、T が継承するすべてのオブジェクトのサイズの合計と、T が仮想的に継承するすべてのオブジェクトの 4 バイトと、T が別の v テーブルを必要とする場合にのみ 4 バイトを加算
  • 注意: クラス K が仮想的に複数回 (任意のレベルで) 継承される場合、K のサイズを 1 回だけ追加する必要があります。

したがって、別の質問に答える必要があります。クラスに別の v テーブルが必要になるのはいつですか?

  • 他のクラスから継承しないクラスには、1 つ以上の仮想メソッドがある場合にのみ v-table が必要です
  • それ以外の場合、仮想継承元ではないクラスに v-table がない場合にのみ、クラスに別の v-table が必要です。

ルールの終わり(テリー・マハフィーが彼の答えで説明したものと一致するように適用できると思います):)

とにかく、私の提案は、Bjarne Stroustrup (C++ の作成者) による次の論文を読むことです。この論文では、仮想継承または非仮想継承で必要な仮想テーブルの数とその理由を正確に説明しています。

それは本当に良い読書です: http://www.hpc.unimelb.edu.au/nec/g1af05e/chap5.html

于 2010-01-10T22:32:17.687 に答える
0

よくわかりませんが、仮想メソッドテーブルへのポインタが原因だと思います

于 2010-01-10T21:47:08.700 に答える