CPP03 1.0
|
virtual
のキーワードをメンバー関数に付ける場合・付けない場合の違い
* 基底クラスのポインタ(または参照)を通じて派生クラスのオブジェクトを操作する場合、virtual 関数は実行時に実際に指しているオブジェクトの型に基づいて呼び出される関数が決まります。
- つまり、コンパイラはどの関数を呼び出すかをコンパイル時には決定せず、プログラムの実行時に決定します。これを動的ディスパッチまたは遅延バインディングと呼びます。
- これにより、基底クラスのインターフェースを通じて、派生クラス固有の振る舞いを呼び出すことが可能になり、ポリモーフィズムを実現できます。
* 派生クラスで基底クラスの virtual 関数と同じシグネチャ(名前、引数の型と数、const 修飾子など)を持つ関数を定義すると、その関数は基底クラスの virtual 関数をオーバーライドします。
- 基底クラスのポインタや参照を通じて派生クラスのオブジェクトのこの関数が呼び出されると、派生クラスでオーバーライドされた関数が実行されます。
* virtual 関数を持つクラスは、通常、コンパイラによって仮想関数テーブル (vtable) と呼ばれる特別なテーブルを持ちます。このテーブルには、そのクラスの仮想関数のアドレスが格納されています。
- オブジェクトは、vtable へのポインタ(通常は vptr - virtual pointer)を内部に持ちます。
- 仮想関数が呼び出される際、この vptr を通じて vtable が参照され、実行時に適切な関数アドレスが特定されて呼び出されます。
* virtual キーワードが付いていないメンバー関数は、コンパイル時にどの関数を呼び出すかが決定されます。
- 基底クラスのポインタ(または参照)を通じて派生クラスのオブジェクトの非仮想関数を呼び出した場合、ポインタ(または参照)の型に基づいて基底クラスの関数が常に呼び出されます。
- 派生クラスに同じ名前の関数があっても、それは隠蔽(hide)されるだけで、基底クラスのポインタや参照を通じて呼び出されることはありません。
* 非仮想関数はオーバーライドされません。派生クラスで同じ名前の関数を定義した場合、それは基底クラスの関数とは別の、独立した関数として扱われます。
* 基底クラスのポインタを通じて派生クラスのオブジェクトを削除する場合、基底クラスのデストラクタが非仮想であると、派生クラスのデストラクタが呼び出されない可能性があります(未定義動作)。これは、派生クラスが独自のメモリ管理やリソース解放を行っている場合に深刻な問題を引き起こす可能性があります。
- 基底クラスのデストラクタを virtual にすることで、常に実際に指しているオブジェクトの型のデストラクタが実行時に呼び出されることが保証されます。これにより、派生クラス固有のクリーンアップ処理が確実に行われ、メモリリークなどの問題を回避できます。
- 継承を行う可能性のある基底クラスのデストラクタは、原則として virtual にするべきです。
特徴 | virtual を付けたメンバー関数 | virtual を付けないメンバー関数 |
---|---|---|
ディスパッチ | 動的 (実行時) | 静的 (コンパイル時) |
バインディング | 遅延 (実行時) | 静的 (コンパイル時) |
ポリモーフィズム | 可能 | 不可能 |
オーバーライド | 可能 | 不可能 (隠蔽される) |
仮想関数テーブル | 通常持つ | 持たない |
デストラクタ | 派生クラスのものが確実に呼ばれる | 派生クラスのものが呼ばれない可能性 |
EX01
でClapTrap
クラスを継承してScavTrap
クラスを作成している場合、attack()
関数が基底クラスでvirtual
であれば、基底クラスのポインタや参照を通じてScavTrap
オブジェクトのattack()
を呼び出した際にScavTrap
でオーバーライドされたattack()
が実行されます。もしattack()
がvirtual
でなければ、常に基底クラス(ClapTrap
) のattack()
が呼び出されてしまいます。
また、将来的にさらにClapTrap
を継承するクラスを作成する可能性がある場合、基底クラスのデストラクタをvirtual
にしておくことは、安全なオブジェクトの破棄のために非常に重要です。
virtual
キーワードは、オブジェクト指向プログラミングにおける柔軟性と拡張性を高めるための重要な機能です。継承関係にあるクラスを扱う際には、どのメンバー関数をvirtual
にすべきかを慎重に検討する必要があります。