269

2 つの C++ クラスがあるとします。

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

次のコードを書くと:

int main()
{
  B b;
  int n = b.getn();
}

2に設定されていると予想されるかもしれませんn

が 1 に設定されていることnがわかりました。なぜでしょうか。

4

15 に答える 15

253

コンストラクタまたはデストラクタから仮想関数を呼び出すことは危険であり、可能な限り避ける必要があります。すべての C++ 実装は、現在のコンストラクターの階層レベルで定義された関数のバージョンを呼び出す必要があり、それ以上は呼び出しません。

これについては、 C++ FAQ Liteのセクション 23.7 でかなり詳細に説明されています。フォローアップのために、それ (および FAQ の残りの部分) を読むことをお勧めします。

抜粋:

[...] コンストラクターでは、派生クラスからのオーバーライドがまだ行われていないため、仮想呼び出しメカニズムが無効になっています。オブジェクトは、「派生前のベース」というベースから構築されます。

[...]

破棄は「基本クラスの前に派生クラス」で行われるため、仮想関数はコンストラクターのように動作します。ローカル定義のみが使用され、オーバーライド関数の呼び出しは行われず、オブジェクトの (現在破棄されている) 派生クラス部分に触れないようにします。

EDITほぼすべてを修正しました (thanks litb)

于 2009-06-07T15:52:28.263 に答える
92

コンストラクターからポリモーフィック関数を呼び出すことは、ほとんどの OO 言語で最悪の事態を引き起こします。この状況が発生した場合、言語が異なれば動作も異なります。

基本的な問題は、すべての言語で、派生型の前に基本型を構築する必要があることです。さて、問題は、コンストラクターからポリモーフィック メソッドを呼び出すとはどういう意味かということです。あなたはそれがどのように振る舞うことを期待していますか?基本レベルでメソッドを呼び出す (C++ スタイル) か、階層の最下部にある構築されていないオブジェクトでポリモーフィック メソッドを呼び出す (Java 方式) という 2 つの方法があります。

C++ では、Base クラスは、独自の構築に入る前に、そのバージョンの仮想メソッド テーブルを構築します。この時点で、仮想メソッドへの呼び出しは、メソッドの基本バージョンを呼び出すか、階層のそのレベルに実装がない場合に呼び出される純粋な仮想メソッドを生成することになります。Base が完全に構築された後、コンパイラは Derived クラスの構築を開始し、階層の次のレベルの実装を指すようにメソッド ポインタをオーバーライドします。

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

Java では、コンパイラは、基本コンストラクターまたは派生コンストラクターに入る前に、構築の最初のステップで同等の仮想テーブルを構築します。意味合いは異なります (そして、私の好みではより危険です)。基本クラスのコンストラクターが派生クラスでオーバーライドされたメソッドを呼び出す場合、その呼び出しは実際には構築されていないオブジェクトのメソッドを呼び出す派生レベルで処理され、予期しない結果が生じます。コンストラクター ブロック内で初期化される派生クラスのすべての属性は、'final' 属性を含め、まだ初期化されていません。クラス レベルで定義されたデフォルト値を持つ要素には、その値が設定されます。

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

ご覧のとおり、ポリモーフィック ( C++ 用語ではvirtual ) メソッドの呼び出しは、よくあるエラーの原因です。C++ では、少なくともまだ構築されていないオブジェクトのメソッドを呼び出さないという保証があります...

于 2009-06-07T16:56:53.117 に答える
68

その理由は、C++ オブジェクトが内部から玉ねぎのように構築されているためです。基本クラスは、派生クラスの前に構築されます。したがって、B を作成する前に、A を作成する必要があります。A のコンストラクターが呼び出されたとき、それはまだ B ではないため、仮想関数テーブルには A の fn() のコピーのエントリがまだあります。

于 2009-06-07T15:46:40.930 に答える
30

C++ FAQ Liteはこれをかなりうまくカバーしています:

基本的に、基本クラス コンストラクターの呼び出し中、オブジェクトはまだ派生型ではないため、派生型ではなく、基本型の仮想関数の実装が呼び出されます。

于 2009-06-07T16:03:12.153 に答える
17

問題の解決策の 1 つは、ファクトリ メソッドを使用してオブジェクトを作成することです。

  • 仮想メソッド afterConstruction() を含むクラス階層の共通基本クラスを定義します。
クラス オブジェクト
{
公衆:
  仮想ボイド afterConstruction() {}
  // ...
};
  • ファクトリ メソッドを定義します。
テンプレート<クラスC>
C* factoryNew()
{
  C* pObject = 新しい C();
  pObject->afterConstruction();

  pObject を返します。
}
  • 次のように使用します。
class MyClass : public オブジェクト
{
公衆:
  仮想ボイド afterConstruction()
  {
    // 何かをします。
  }
  // ...
};

MyClass* pMyObject = factoryNew();

于 2009-06-07T17:09:10.293 に答える
1

Windowsエクスプローラーからのクラッシュエラーを知っていますか?! 「純粋仮想関数呼び出し…」
同じ問題…

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

関数 pureVitualFunction() の実装がなく、関数がコンストラクターで呼び出されるため、プログラムはクラッシュします。

于 2009-06-07T16:51:29.630 に答える
1

vtables はコンパイラによって作成されます。クラス オブジェクトには、その vtable へのポインターがあります。起動すると、その vtable ポインターは基本クラスの vtable を指します。コンストラクター コードの最後で、コンパイラーはコードを生成して、vtable ポインターをクラスの実際の vtable に再ポイントします。これにより、仮想関数を呼び出すコンストラクター コードが、クラスのオーバーライドではなく、それらの関数の基本クラスの実装を呼び出すことが保証されます。

于 2013-05-07T10:14:14.050 に答える
-2

ここでは仮想キーワードの重要性を認識していません。b は静的型付き変数であり、その型はコンパイル時にコンパイラによって決定されます。関数呼び出しは vtable を参照しません。b が構築されると、その親クラスのコンストラクターが呼び出されるため、_n の値が 1 に設定されます。

于 2013-10-28T06:58:10.737 に答える
-4

オブジェクトのコンストラクターの呼び出し中に、仮想関数ポインター テーブルが完全に構築されていません。これを行うと、通常、期待どおりの動作が得られません。この状況での仮想関数の呼び出しは機能する可能性がありますが、保証されておらず、移植性を確保して C++ 標準に従うためには避ける必要があります。

于 2009-06-07T17:00:14.470 に答える