C++ の仮想関数

C++ の仮想関数

仮想関数 (仮想メソッドとも呼ばれます) は、基本クラス内で宣言され、派生クラスによって再定義 (オーバーライド) されるメンバー関数です。ポインターまたは基本クラスへの参照を使用して派生クラス オブジェクトを参照すると、そのオブジェクトの仮想関数を呼び出し、派生クラスのバージョンのメソッドを実行できます。

  • 仮想関数は、関数呼び出しに使用される参照 (またはポインター) の種類に関係なく、オブジェクトに対して正しい関数が呼び出されることを保証します。
  • これらは主に、ランタイムポリモーフィズムを実現するために使用されます。
  • 関数は次のように宣言されます。 バーチャル 基本クラスのキーワード。
  • 関数呼び出しの解決は実行時に行われます。

仮想機能のルール

C++ の仮想関数の規則は次のとおりです。

  1. 仮想関数を静的にすることはできません。
  2. 仮想関数は、別のクラスのフレンド関数になることができます。
  3. 実行時ポリモーフィズムを実現するには、基本クラス型のポインターまたは参照を使用して仮想関数にアクセスする必要があります。
  4. 仮想関数のプロトタイプは、基本クラスでも派生クラスでも同じである必要があります。
  5. これらは常に基本クラスで定義され、派生クラスでオーバーライドされます。派生クラスが仮想関数をオーバーライド (または再定義) することは必須ではありません。その場合、関数の基本クラス バージョンが使用されます。
  6. クラスは仮想デストラクターを持つことができますが、仮想コンストラクターを持つことはできません。

仮想関数のコンパイル時 (アーリー バインディング) VS ランタイム (レイト バインディング) の動作

仮想関数の実行時の動作を示す次の単純なプログラムを考えてみましょう。

C++




// C++ program to illustrate> // concept of Virtual Functions> #include> using> namespace> std;> class> base {> public> :> > virtual> void> print() { cout < <> 'print base class '> ; }> > void> show() { cout < <> 'show base class '> ; }> };> class> derived :> public> base {> public> :> > void> print() { cout < <> 'print derived class '> ; }> > void> show() { cout < <> 'show derived class '> ; }> };> int> main()> {> > base* bptr;> > derived d;> > bptr = &d;> > // Virtual function, binded at runtime> > bptr->print();>> return> 0;> }>

出力

print derived class show base class 

説明: 実行時のポリモーフィズムは、基本クラス型のポインター (または参照) を通じてのみ実現されます。また、基底クラス ポインターは、基底クラスのオブジェクトだけでなく、派生クラスのオブジェクトも指すことができます。上記のコードでは、基本クラス ポインター「bptr」には、派生クラスのオブジェクト「d」のアドレスが含まれています。

print() 関数が仮想関数で宣言されているため、遅延バインディング (実行時) はポインタの内容 (つまり、ポインタが指す位置) に従って行われ、早期バインディング (コンパイル時) はポインタのタイプに従って行われます。キーワードなので、実行時にバインドされます (出力は 派生クラスを出力します ポインターが派生クラスのオブジェクトを指しているため)、show() は非仮想であるため、コンパイル時にバインドされます (出力は 基本クラスを表示 ポインタは基本型であるため)。

注記: 基本クラスで仮想関数を作成し、それが派生クラスでオーバーライドされる場合、派生クラスで virtual キーワードは必要ありません。関数は派生クラスでは自動的に仮想関数とみなされます。

仮想機能の働き (VTABLE と VPTR の概念)

ここで説明したように、クラスに仮想関数が含まれている場合、コンパイラ自体は 2 つのことを行います。

  1. そのクラスのオブジェクトが作成されると、 仮想ポインタ (VPTR) は、そのクラスの VTABLE を指すクラスのデータ メンバーとして挿入されます。作成された新しいオブジェクトごとに、新しい仮想ポインタがそのクラスのデータ メンバーとして挿入されます。
  2. オブジェクトが作成されたかどうかに関係なく、クラスにはメンバーとして次のものが含まれます。 VTABLE と呼ばれる関数ポインターの静的配列 。このテーブルのセルには、そのクラスに含まれる各仮想関数のアドレスが格納されます。

以下の例を考えてみましょう。

仮想ポインタと仮想テーブル

C++




// C++ program to illustrate> // working of Virtual Functions> #include> using> namespace> std;> class> base {> public> :> > void> fun_1() { cout < <> 'base-1 '> ; }> > virtual> void> fun_2() { cout < <> 'base-2 '> ; }> > virtual> void> fun_3() { cout < <> 'base-3 '> ; }> > virtual> void> fun_4() { cout < <> 'base-4 '> ; }> };> class> derived :> public> base {> public> :> > void> fun_1() { cout < <> 'derived-1 '> ; }> > void> fun_2() { cout < <> 'derived-2 '> ; }> > void> fun_4(> int> x) { cout < <> 'derived-4 '> ; }> };> int> main()> {> > base* p;> > derived obj1;> > p = &obj1;> > // Early binding because fun1() is non-virtual> > // in base> > p->fun_1();>>'

出力

base-1 derived-2 base-3 base-4 

説明: 最初に、型基本クラスのポインターを作成し、それを派生クラス オブジェクトのアドレスで初期化します。派生クラスのオブジェクトを作成すると、コンパイラは、派生クラスの VTABLE のアドレスを含むクラスのデータ メンバーとしてポインターを作成します。

同様の概念 遅延バインディングと早期バインディング 上記の例のように使用されます。 fun_1() 関数呼び出しの場合、関数の基本クラス バージョンが呼び出されます。 fun_2() は派生クラスでオーバーライドされるため、派生クラス バージョンが呼び出されます。 fun_3() は派生クラスでオーバーライドされず、仮想関数です。したがって、基本クラスのバージョンが呼び出されます。同様に、 fun_4() はオーバーライドされないため、基本クラスのバージョンが呼び出されます。

注記: 派生クラスの fun_4(int) は、基本クラスの仮想関数 fun_4() とは異なります。これは、両方の関数のプロトタイプが異なるためです。

仮想機能の制限

    遅い: 仮想メカニズムにより関数呼び出しに若干時間がかかり、コンパイラはコンパイル時にどの関数が呼び出されるのか正確にわからないため、最適化がより困難になります。デバッグが難しい: 複雑なシステムでは、仮想関数により、関数がどこから呼び出されているかを把握することが少し難しくなる場合があります。