はじめに
C++の継承では、親クラス(基底クラス)へのポインタを使って、子クラス(派生クラス)のオブジェクトを指し示すことができます。しかし、ここで一つ問題があります。
親クラスのポインタ->関数()
のように呼び出した場合、通常は**ポインタの型(親クラス)**で定義された関数が呼び出されてしまいます。たとえ子クラスでその関数がオーバーライドされていても、子クラス版の関数は呼ばれません。
この問題を解決し、ポインタが実際に指しているオブジェクトの種類に応じて、適切なバージョンの関数を呼び出す仕組みが「仮想関数 (virtual function
)」であり、それによって実現される性質が、オブジェクト指向の三本柱の一つ「多態性(ポリモーフィズム)」です。
仮想関数を使ったサンプルコード
このコードは、Character
(親クラス)と、それを継承した Warrior
, Mage
(子クラス)を定義します。attack
関数を仮想関数として定義し、各クラスでオーバーライドします。
完成コード
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// --- 親クラス(基底クラス) ---
class Character {
protected:
string name;
public:
Character(string n) : name(n) {}
// 1. virtualを付けて、仮想関数として宣言
virtual void attack() {
cout << name << " の攻撃!...しかし何も起こらなかった。" << endl;
}
};
// --- 子クラス(派生クラス) ---
class Warrior : public Character {
public:
// 親クラスのコンストラクタを呼び出す
Warrior(string n) : Character(n) {}
// 2. 親クラスの仮想関数をオーバーライド
void attack() {
cout << name << " は剣を振った!" << endl;
}
};
class Mage : public Character {
public:
Mage(string n) : Character(n) {}
// 2. 親クラスの仮想関数をオーバーライド
void attack() {
cout << name << " は魔法を唱えた!" << endl;
}
};
int main() {
// 3. 親クラスへのポインタの配列(vector)を作成
vector<Character*> party;
party.push_back(new Warrior("戦士"));
party.push_back(new Mage("魔法使い"));
cout << "--- パーティの全メンバーが攻撃! ---" << endl;
// 4. 同じ呼び出し方でも、オブジェクトの種類によって動作が変わる
for (Character* p_char : party) {
p_char->attack(); // ポインタ経由でattack()を呼び出し
}
// メモリの解放
for (Character* p_char : party) {
delete p_char;
}
return 0;
}
実行結果
--- パーティの全メンバーが攻撃! ---
戦士 は剣を振った!
魔法使い は魔法を唱えた!
コードの解説
1. virtual void attack()
親クラス Character
の attack
関数の宣言の先頭に virtual
を付けています。これにより、attack
関数は仮想関数となります。
2. void attack()
(子クラス内)
子クラス Warrior
と Mage
で、それぞれ独自の attack
関数をオーバーライドしています。
3. 親クラスへのポインタの配列 (vector<Character*>
)
party
という vector
(動的配列)は、Character
クラスへのポインタを格納します。ここに、Warrior
オブジェクトのアドレスと Mage
オブジェクトのアドレスを入れています。(new
を使って動的にオブジェクトを生成しています)
4. p_char->attack()
これが、**多態性(ポリモーフィズム)**が機能している核心部分です。
- ループ変数
p_char
の型は、常にCharacter*
(親クラスへのポインタ)です。 - にもかかわらず、
p_char->attack()
を呼び出すと、p_char
がWarrior
オブジェクトを指しているときは、Warrior
版のattack()
が実行されます。p_char
がMage
オブジェクトを指しているときは、Mage
版のattack()
が実行されます。
これは、attack
関数が仮想関数であるため、C++コンパイラが「ポインタの型ではなく、ポインタが実際に指しているオブジェクトの型を見て、呼び出すべき最適な関数を、プログラムの実行時に決定する」という特別な動作(動的ディスパッチ)を行っているからです。
もし、親クラスの attack
関数から virtual
キーワードを外してしまうと、実行結果は2回とも「〜の攻撃!…しかし何も起こらなかった。」となり、子クラスのオーバーライドした関数は呼ばれません。
まとめ
今回は、C++の仮想関数と、それによって実現される多態性(ポリモーフィズム)について解説しました。
- 親クラスで、子クラスにオーバーライドされる可能性のある関数には
virtual
を付ける。 virtual
を付けた関数(仮想関数)は、親クラスへのポインタ経由で呼び出された場合でも、ポインタが実際に指しているオブジェクトの型でオーバーライドされた関数が実行される。- これにより、同じインターフェース(呼び出し方)で、異なるオブジェクトに異なる振る舞いをさせることができる(多態性)。
仮想関数は、オブジェクト指向の柔軟性と拡張性を支える、非常に重要な仕組みです。