4

私は2つのクラスを持っています.1つは基本クラスで、もう1つはそれから派生したものです:

class base {

 int i ;

  public :
  virtual ~ base () { }
};

class derived :  virtual public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

ここでの答えは 16 です。しかし、代わりに非仮想パブリック継承を行うか、基本クラスを非ポリモーフィックにすると、答えは 12 になります。

class base {

 int i ;

 public :
virtual ~ base () { }
};

class derived :  public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

また

class base {

int i ;

public :
~ base () { }
};

class derived :  virtual public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

どちらの場合も、答えは 12 です。

1番目と他の2つのケースで派生クラスのサイズに違いがある理由を誰かが説明できますか?

(誰かが本当にこれを必要とする場合、私はコード::ブロック10.05に取り組んでいます)

4

6 に答える 6

3

クラスに仮想関数がある場合、このクラスのオブジェクトには vptr が必要です。これは、正しい仮想関数のアドレスが見つかる仮想テーブルである vtable へのポインタです。呼び出される関数は、オブジェクトが基本サブオブジェクトである最も派生したクラスである、オブジェクトの動的タイプに依存します。

派生クラスは基本クラスから実質的に継承するため、派生クラスに対する基本クラスの位置は固定されておらず、オブジェクトの動的な型にも依存します。gcc では、仮想基底クラスを持つクラスには、基底クラスを見つけるために vptr が必要です (仮想関数がない場合でも)。

また、基本クラスには、基本クラス vptr の直後に配置されるデータ メンバーが含まれます。基本クラスのメモリ レイアウトは { vptr, int}です。

基本クラスに vptr が必要な場合、それから派生したクラスにも vptr が必要になりますが、多くの場合、基本クラスのサブオブジェクトの「最初の」vptr が再利用されます (再利用された vptr を持つこの基本クラスはプライマリ ベースと呼ばれます)。ただし、派生クラスは、仮想関数の呼び出し方法だけでなく、仮想ベースの場所を決定するために vptr を必要とするため、この場合は不可能です。派生クラスは、vptr を使用しないと仮想基本クラスを見つけることができません。仮想基本クラスがプライマリ ベースとして使用された場合、派生クラスは vptr を読み取るためにそのプライマリ ベースを見つける必要があり、そのプライマリ ベースを見つけるために vptr を読み取る必要があります

したがって、派生物は基本ベースを持つことができず、独自の vptr を導入します

したがって、型の基本クラス サブオブジェクトレイアウトは次のようになります。、オフセットとして表されます。derivedintbase

型の完全なオブジェクトのレイアウトderivedは次のとおりです。derivedbase

したがって、可能な最小サイズderivedは (2 int+ 2 vptr) または一般的な ptr = int= ワード アーキテクチャの 4 ワード、またはこの場合は 16 バイトです。(そして、Visual C++ はより大きなオブジェクトを作成します (仮想基本クラスが関与する場合)。私は、derivedポインタがもう 1 つあると考えています。)

そうです、仮想関数にはコストがかかり、仮想継承にはコストがかかります。この場合の仮想継承のメモリ コストは、オブジェクトごとに 1 つ多くのポインターです。

多くの仮想基本クラスを含む設計では、オブジェクトあたりのメモリ コストは、仮想基本クラスの数に比例する場合もあれば、そうでない場合もあります。コストを見積もるには、特定のクラス階層について話し合う必要があります。

複数の継承や仮想基本クラス (さらには仮想関数) を持たない設計では、コンパイラによって自動的に行われる多くのことをエミュレートする必要があるかもしれません。一連のポインター、場合によっては関数へのポインター、場合によってはオフセット...紛らわしく、エラーが発生しやすい。

于 2012-08-05T07:36:06.503 に答える
3

ここには、余分なオーバーヘッドを引き起こす 2 つの別個の要因があります。

まず、基本クラスに仮想関数があると、仮想メソッド テーブルへのポインターを格納する必要があるため、そのサイズがポインター サイズ (この場合は 4 バイト) だけ増加します。

normal inheritance with virtual functions:

0        4       8       12
|      base      |
| vfptr  |  i    |   j   |

derived第 2 に、仮想継承では、base. 通常の継承では、 と の間のオフセットderivedbaseコンパイル時の定数 (単一継承の場合は 0) です。仮想継承では、オフセットはオブジェクトの実行時の型と実際の型の階層に依存する場合があります。実装は異なる場合がありますが、たとえば Visual C++ では次のようになります。

virtual inheritance with virtual functions:

0        4         8        12        16
                   |      base        |
|  xxx   |   j     |  vfptr |    i    |

xxxは、 へのオフセットを決定できる型情報レコードへのポインタですbase

そしてもちろん、仮想関数なしで仮想継承を持つことも可能です:

virtual inheritance without virtual functions:

0        4         8        12
                   |  base  |
|  xxx   |   j     |   i    |
于 2012-06-05T21:17:56.733 に答える
2

実行時にクラス タイプをマークするために、さらに 4 バイトが必要になる可能性があります。例えば:

class A {
 virtual int f() { return 2; }
}

class B : virtual public A {
 virtual int f() { return 3; }
}

int call_function( A *a) {
   // here we don't know what a really is (A or B)
   // because of this to call correct method
   // we need some runtime knowledge of type and storage space to put it in (extra 4 bytes).
   return a->f();
}

int main() {
   B b;
   A *a = (A*)&b;

   cout << call_function(a);
}
于 2012-06-05T19:47:29.517 に答える
2

何が起こっているかというと、クラスを仮想メンバーを持つか、仮想継承を伴うものとしてマークするために使用される追加のオーバーヘッドです。追加の量はコンパイラによって異なります。

注意点: デストラクタが仮想でないクラスからクラスを派生させると、通常、問題が発生します。大きな問題。

于 2012-06-05T19:34:45.217 に答える
2

仮想継承のポイントは、基底クラスを共有できるようにすることです。問題は次のとおりです。

struct base { int member; virtual void method() {} };
struct derived0 : base { int d0; };
struct derived1 : base { int d1; };
struct join : derived0, derived1 {};
join j;
j.method();
j.member;
(base *)j;
dynamic_cast<base *>(j);

最後の 4 行はすべてあいまいです。派生0内のベースが必要か、派生1内のベースが必要かを明示的に指定する必要があります。

2 行目と 3 行目を次のように変更すると、問題は解決します。

struct derived0 : virtual base { int d0; };
struct derived1 : virtual base { int d1; };

j オブジェクトには base のコピーが 2 つではなく 1 つしかないため、最後の 4 行があいまいになりません。

しかし、それをどのように実装する必要があるかを考えてください。通常、derived0 では d0 が m の直後に来て、derived1 では d1 が m の直後に来ます。ただし、仮想継承では、両方とも同じ m を共有するため、d0 と d1 の両方をその直後に置くことはできません。したがって、何らかの形式の追加の間接化が必要になります。それが追加のポインタの由来です。

レイアウトが何であるかを正確に知りたい場合は、ターゲット プラットフォームとコンパイラによって異なります。「gcc」だけでは不十分です。しかし、Windows 以外の最新のターゲットの多くについては、答えは Itanium C++ ABI によって定義されています

于 2012-06-05T21:29:43.343 に答える
0

余分なサイズは、このクラスの特定のオブジェクトまたはその子孫/祖先のメンバー関数ポインターを保持するために、クラスに「見えない」ように追加された vtable/vtable ポインターによるものです。

それが明確でない場合は、C++ での仮想継承についてさらに読む必要があります。

于 2012-06-05T20:17:17.910 に答える