C++の基礎 : 仮想関数
仮想関数
継承と仮想関数を使うことで、既存のクラスの振る舞いをカスタマイズした新しいクラスを定義できます。
仮想関数の宣言
時計を表す Clock クラスを定義してみましょう。この時計は Tick() 関数を呼び出すたびに時を刻んでいき、時を 60 回刻むとピーピー鳴く時計です。
class Clock { protected: SIntN time; // 時刻を表すメンバ変数 public: Clock() { // Clock オブジェクトを作成時に時刻を 0 に初期化 time = 0; } // 現在の時刻を返す SIntN GetTime() { return time; } // 時刻を 1 つすすめる Void Tick() { ++time; if (time == 60) Action(); } // 60 回目に実行される関数 virtual Void Action() { // 時計がピーピー鳴る処理 } };
これを見ておわかりのように、ピーピー鳴く処理は Action() 関数により提供されます。この関数には virtual というキーワードがついています。これが仮想関数と呼ばれるものです。
仮想関数のオーバーライド
仮想関数には、派生クラスで再定義できる、という利点があります。
たとえばここに、笑い時計があるとしましょう。この時計は 60 回時を刻むとケタケタと笑い出します。このようなクラスは、Clock クラスから派生して Action() 関数を再定義することにより、簡単に実装できます。
class LaughClock : public Clock { public: virtual Void Action() { // 時計がケタケタ笑う処理 } };
たったこれだけで笑い時計が実装できるのです。時を刻むためのメンバ変数やメンバ関数は Clock クラスを継承することですでに実装されています。LaughClock クラスで行いたいのは、Action() 関数の内容をピーピーではケタケタに変えることです。基底クラスの仮想関数を再定義することにより、基底クラスの振る舞いをカスタマイズできるのです。
このように、基底クラスの仮想関数を派生クラスで再定義することをオーバーライドといいます。
仮想関数の呼び出し
仮想関数は通常のメンバ関数と同じように呼び出します。ここで注意していただきたいのは、仮想関数の呼び出しは、オブジェクトの (実際の) クラスが何であるかにより、呼び出される関数が異なるということです。
Clock clock; clock.Action(); // clock オブジェクトのクラスは Clock であるため、 // Clock::Action() が呼び出され、ピーピー鳴きます。 LaughClock baby; baby.Action(); // baby オブジェクトのクラスは LaughClock であるため、 // LaughClock::Action() が呼び出され、ケタケタ笑います。 Clock * boy = new LaughClock(); boy->Action(); // boy オブジェクトのみかけのクラスは Clock だが、 // 実際のクラスは LaughClock であるため、 // LaughClock::Action() が呼び出され、ケタケタ笑います。
メンバ関数の上書き
このような疑問をもたれるかも知れません。Clock クラスの Action() メンバ関数を仮想関数にしなくても、LaughClock を実装できるんじゃないの?
class Clock { ... // 60 回目に実行される関数 Void Action() // 仮想関数ではない { // 時計がピーピー鳴る処理 } }; class LaughClock : public Clock { public: Void Action() // 仮想関数ではない { // 時計がケタケタ笑う処理 } };
これではうまくいかないのです。この場合、Action() 関数が仮想的でないと宣言されていますから、Tick() 関数からの Action() の呼び出しは、仮想関数呼び出し (派生クラスで再定義された関数の呼び出し) とは解釈されず、単純に Clock クラスの Action() メンバ関数を呼び出すと解釈されます。ですから、このような実装では、笑い時計がピーピー鳴くことになってしまいます。
class Clock { ... // 時刻を 1 つすすめる Void Tick() { ++time; if (time == 60) Action(); // ここで呼び出す関数が仮想関数として宣言されているかどうかにより // 自分のクラスの Action() を呼び出すのか、 // 派生クラスの Action() を呼び出すのかが決まります。 } ... };
このように、基底クラスの仮想的でないメンバ関数を再定義することを上書きといいます。
派生クラスで振る舞いをカスタマイズさせたいのであれば、通常のメンバ関数を上書きするのではなく、仮想関数を宣言してそれをオーバライドしなければなりません。
【Java では】Java では、クラスのすべてのメソッドは仮想関数です。
この章のまとめ
- 仮想関数はメンバ関数の宣言に virtual キーワードをつけることで宣言できます。
- 仮想関数は派生クラスで再定義することで、クラスの振る舞いをカスタマイズできます。
やってみよう
次のようなコードを実行してみましょう!
DoAction() で実行される関数が、オブジェクトにより異なることを確認しましょう。Action() が virtual でない場合、DoAction はどのような関数呼び出しになるでしょうか?
Void DoAction(Clock* pclock) { pclock->Action(); } Void TestVirtual(Void) { Clock pee; pee.Action(); LaughClock keta; keta.Action(); DoAction(&pee); DoAction(&keta); }