C++やObject Pascalなどでよく用いられるオブジェクト指向プログラミングにおいて、仮想関数または仮想メソッドとは、継承可能かつオーバーライド可能な関数またはメソッドであり、動的にディスパッチされます。仮想関数は、オブジェクト指向プログラミング(OOP)における(実行時)ポリモーフィズムの重要な要素です。これにより、コンパイル時に正確に特定されなかった対象関数の実行が可能になります。
Javaなどの多くのプログラミング言語では、デフォルトですべてのメソッドが仮想メソッドとして扱われます。[ 1 ] Pythonなどの一部の言語では、この動作を変更することはできません。[ 2 ]ただし、派生クラスによるメソッドのオーバーライドを防ぐ修飾子を提供する言語もあります ( Java [ 3 ]およびPHP [ 4 ]のfinalおよびprivateキーワードなど)。
仮想関数の概念は、以下の問題を解決する。
オブジェクト指向プログラミングでは、派生クラスが基底クラスを継承する場合、派生クラスのオブジェクトは、派生クラスの型ではなく、基底クラスの型のポインタまたは参照を介して参照されることがあります。派生クラスによって基底クラスのメソッドがオーバーライドされている場合、そのような参照またはポインタによって実際に呼び出されるメソッドは、ポインタまたは参照の宣言された型に応じて「早期」(コンパイラによる)にバインド(リンク)されるか、参照されるオブジェクトの実際の型に応じて「後期」(つまり、言語の実行時システムによる)にバインド(リンク)されます。
仮想関数は「遅延解決」されます。基底クラスで対象の関数が「仮想」である場合、ポインタまたは参照の宣言型に関係なく、参照先のオブジェクトの実際の型に基づいて、最も派生したクラスの関数の実装が呼び出されます。仮想でない場合は、メソッドは「早期解決」され、ポインタまたは参照の宣言型に基づいて選択されます。
仮想関数を使うと、プログラムは、コードがコンパイルされた時点では必ずしも存在しないメソッドを呼び出すことができる。
C++ では、仮想メソッドは基底クラスの関数宣言の前にキーワードを付加することで宣言されますvirtual。この修飾子は派生クラスのそのメソッドのすべての実装に継承されるため、派生クラスは互いにオーバーライドを続け、遅延バインドすることができます。また、基底クラスが所有するメソッドが仮想メソッドを呼び出す場合でも、代わりに派生メソッドが呼び出されます。オーバーロードは、1 つのクラスに同じメソッド名を持つがパラメータが異なる 2 つ以上のメソッドがある場合に発生します。オーバーライドは、同じメソッド名とパラメータを持つ 2 つのメソッドを持つことを意味します。オーバーロードは関数マッチングとも呼ばれ、オーバーライドは動的関数マッピングとも呼ばれます。

例えば、基底クラスにAnimalは仮想関数がありますeat。サブクラスはサブクラスとは異なるLlama実装をしますが、Animal と呼ばれる任意のクラスインスタンスで呼び出すことで、特定のサブクラスの動作を取得できます。eatWolfeateat
import std ;class Animal { public : // 意図的に仮想関数ではない: void move () { std :: println ( "この動物は何らかの方法で動きます" ); }virtual void eat () = 0 ; };// クラス "Animal" は、必要に応じて Eat の定義を持つことができます。class Llama : public Animal { public : // 非仮想関数 Move は継承されますが、オーバーライドされません。void eat () override { std :: println ( "ラマは草を食べます!" ); } };これにより、プログラマーはクラスのオブジェクトのリストを処理しAnimal、リストにどのeatような種類の動物が含まれているか、各動物がどのように食べるか、または可能な動物の種類が全てであるかどうかを知る必要なく、各オブジェクトに順番に食べるように指示することができます(を呼び出すことによって)。
C言語では、仮想関数の仕組みは次のように実現できます。
#include <stdio.h>// オブジェクトは自身のクラスを指す... typedef struct Animal { const struct AnimalVTable * vtable ; } Animal ;// ...仮想関数 Animal.eat を含むtypedef struct AnimalVTable { void ( * eat )( Animal * self ); // '仮想' 関数} AnimalVTable ;/* Animal.move は仮想関数ではないため、 上記の構造体には含まれていません。*/ void move ( const Animal * self ) { printf ( "<Animal at %p> が何らかの方法で移動しました\n " , ( void * )( self )); }/* Animal.moveを直接実行するmoveとは異なり、 eat関数はコンパイル時にどの関数(存在する場合)を呼び出すべきかを知ることができません。Animal.eat はeatが呼び出された実行時にのみ解決できます。*/ void eat ( Animal * self ) { const AnimalVTable * vtable = self -> vtable ; if ( vtable -> eat ) { ( * vtable -> eat )( self ); // Animal.eatを実行} else { fprintf ( stderr , "'eat'仮想メソッドが実装されていません\n " ); } }/* Llama.eat の実装。これは 'void eat(Animal* self)' から呼び出される 対象関数です。 */ static void Llama_eat ( Animal * self ) { printf ( "<Llama at %p> ラマは草を食べます!\n " , ( void * )( self )); }// クラスを初期化しますconst AnimalVTable Animal_vt = { NULL }; // 基底クラスは Animal.Eat を実装していませんconst AnimalVTable Llama_vt = { Llama_eat }; // しかし派生クラスは実装していますint main ( void ) { // オブジェクトをそのクラスのインスタンスとして初期化するAnimal animal = { & Animal_vt }; Animal llama = { & Llama_vt }; move ( & animal ); // Animal.move move ( & llama ); // Llama.move eat ( & animal ); // Animal.eat は解決できないため、stderr に "Not Implemented" と出力するeat ( & llama ); // Llama.eat を解決して実行する}純粋仮想関数または純粋仮想メソッドとは、派生クラスが抽象クラスでない場合、派生クラスによって実装される必要がある仮想関数です。純粋仮想メソッドを含むクラスは「抽象クラス」と呼ばれ、直接インスタンス化することはできません。抽象クラスのサブクラスは、継承されたすべての純粋仮想メソッドがそのクラスまたは親クラスによって実装されている場合にのみ、直接インスタンス化できます。純粋仮想メソッドは通常、宣言(シグネチャ)を持ち、定義(実装)はありません。
例えば、抽象基底クラスはMathSymbol純粋仮想関数を提供しdoOperation()、派生クラスPlusとがMinus実装してdoOperation()具体的な実装を提供する場合があります。クラスdoOperation()内でを実装することは意味がありません。はMathSymbol、MathSymbolその動作がの特定の種類(サブクラス)ごとにのみ定義される抽象概念だからですMathSymbol。同様に、の特定のサブクラスは、MathSymbolの実装なしでは完全ではありません doOperation()。
純粋仮想メソッドは通常、それを宣言するクラスに実装を持ちませんが、一部の言語(C++やPythonなど)では、純粋仮想メソッドは宣言するクラスに実装を持つことが許可されており、派生クラスが適切な場合に委譲できるフォールバックまたはデフォルトの動作を提供します。[ 5 ] [ 6 ]
純粋仮想関数は、メソッド宣言を使用してインターフェースを定義する場合にも使用できます。これは、Java の interface キーワードが明示的に指定するのと同様です。このような使用法では、派生クラスがすべての実装を提供します。このような設計パターンでは、インターフェースとして機能する抽象クラスには純粋仮想関数のみが含まれ、データメンバや通常のメソッドは含まれません。C++ では、多重継承をサポートしているため、このような純粋抽象クラスをインターフェースとして使用できます。しかし、多くのオブジェクト指向言語は多重継承をサポートしていないため、多くの場合、別のインターフェースメカニズムを提供します。Javaプログラミング言語はその一例です。
オブジェクトのコンストラクタまたはデストラクタの実行中の動作は、プログラミング言語によって異なります。そのため、コンストラクタ内で仮想関数を呼び出すことは一般的に推奨されません。
C++ では、「基本」関数が呼び出されます。具体的には、現在のコンストラクタまたはデストラクタのクラスよりも派生していない最も派生した関数が呼び出されます。[ 7 ] : §15.7.3 [ 8 ] [ 9 ]その関数が純粋仮想関数である場合、未定義の動作が発生します。[ 7 ] : §13.4.6 [ 8 ]純粋仮想関数の呼び出しは明示的に修飾する必要があるため、クラスにその純粋仮想関数の実装が含まれている場合でも、これは当てはまります。[ 10 ]準拠する C++ 実装は、コンパイル時またはリンク時に純粋仮想関数への間接呼び出しを検出する必要はありません (そして一般的には検出できません) 。一部のランタイム システムでは、実行時に純粋仮想関数への呼び出しに遭遇すると、純粋仮想関数呼び出しエラーを発行します。
Java や C# では、派生実装が呼び出されますが、派生コンストラクタによって初期化されていないフィールドもあります (ただし、デフォルト値のゼロ値に初期化されます)。[ 11 ]抽象ファクトリーパターンなどの一部のデザインパターンは、この機能をサポートする言語でこの使用を積極的に推奨しています。
オブジェクト指向言語は通常、オブジェクトの作成時と破棄時にメモリの割り当てと解放を自動的に管理します。ただし、一部のオブジェクト指向言語では、必要に応じてカスタムデストラクタメソッドを実装できます。対象の言語が自動メモリ管理を使用している場合、呼び出されるカスタムデストラクタ(この文脈では一般的にファイナライザと呼ばれます)は、対象のオブジェクトに適したものであることが保証されます。たとえば、Animal を継承する Wolf 型のオブジェクトが作成され、両方にカスタムデストラクタが設定されている場合、呼び出されるのは Wolf で宣言されたデストラクタになります。
手動メモリ管理のコンテキストでは、特に静的ディスパッチに関連して、状況はより複雑になる可能性があります。Wolf型のオブジェクトが作成され、Animal型のポインタによって指されている場合、削除されるのがこのAnimal型のポインタである場合、デストラクタが仮想でない限り、呼び出されるデストラクタはWolf型ではなくAnimal型に定義されているものになる可能性があります。これは特にC++で顕著であり、デストラクタが仮想でない場合、この動作はプログラミングエラーの一般的な原因となります。