ショートカット
ファシリテーター × あり方
コーディングの向こう側
Hello, ANOTHER world!
オブジェクト指向のはなし
プログラミングのはなし
C言語実力診断クイズ
eSkillBooks
オブジェクト指向のはなし

ポリモーフィズムの記述

導入

ポリモーフィズムを実現するには、スーパークラスと同じ形式のメソッドを定義し、これを仮想関数にします。またデストラクタも仮想関数にします。そしてスーパークラスの型のポインタを通してアクセスします。

用語
このトピックでは次の用語を覚えます:
  • virtual キーワード
  • 仮想関数
解説

前のトピックで「留守番電話」クラスの例題をとりあげました。そのサンプルコードの中に、virtual というキーワードがありましたが、これはポリモーフィズムを表現するためのものです。virtual な関数のことを仮想関数と呼びます。

SAMPLE CODE
Phone クラス宣言ファイル(ファイル名:"Phone.h")

#ifndef INCLUDED_Phone
#define INCLUDED_Phone

class Phone {
public:
    Phone();
    virtual ~Phone();
    
// 電話の基本的機能に関するメソッド
public:
    // 電話をかける
    bool MakeCall( int nToSomebody );
    
    // 電話を受信する(イベント処理)
    virtual bool OnReceiveCall( int nFromSomebody );
};

#endif // #ifndef INCLUDED_Phone

上のコードは前のトピックで掲載したものと同じです。これは「電話」クラス(「留守番電話」のスーパークラス)の宣言ですが、2カ所に virtual が使用されています。

ひとつは OnReceiveCall というメソッドです。このメソッドの動作は相手から電話がかかってきたときの処理を行うというものです。おそらく受信を管理するイベントループがあって、そこから呼び出されるのでしょう。

重要なのは、「留守番電話」クラスにもまったく同じインターフェイスのメソッドがあって、その動作が異なる点です。しかしどちらも「電話」 の一種であり、メソッドを実行する側では同じように扱うことができます。メソッドを実行する側では、たとえば次のようになります。スーパークラス(Phone)型のポインタを通して操作している点に注目してください。C++ ではこのようにすることで、ポリモーフィックなメソッド呼び出しを実現します。

SAMPLE CODE
// 受信イベントを発生させる
void ReceiveCall( Phone *pPhone )
{
    pPhone->OnReceiveCall(0);
}

この ReceiveCall 関数を呼び出すときのパラメータは「電話」オブジェクトでなくても、「電話」のサブクラスのオブジェクトなら何でもかまいません。「留守番電話」でも良いし、ほかのクラスでも良いのです(「電話」を継承していることだけが条件です)。たとえばユーザーサポートを効率的に行うために自動応答する電話や、いたずら電話を撃退する機能が付いた電話でも良いのです。具体的には、たとえば次のようにします。

SAMPLE CODE
#include <stdlib.h>
#include "Answerphone.h"

// 受信イベントを発生させる
void ReceiveCall( Phone *pPhone );

// ポリモーフィズムの実験
int main()
{
    // 「電話」の場合
    {
        Phone thePhone;
        ReceiveCall( &thePhone );
    }
    
    // 「留守番電話」の場合
    {
        Answerphone thePhone;
        ReceiveCall( &thePhone );
    }
    
    return EXIT_SUCCESS;	
}
デストラクタを virtual にする

「電話」クラスでもうひとつ仮想関数になっているものがありました。それはデストラクタ ~Phone です。一般に、ポリモーフィズムを前提としたスーパークラスのデストラクタは virtual にすべきです。その理由は delete 演算子にあります。

ポリモーフィックなオブジェクトはスーパークラスのインスタンスであるかのように扱われることは上述した通りです。「留守番電話」オブジェクトを new で確保し、これを「電話」クラス型のポインタを通して delete する場合を考えてください。デストラクタが virtual でない場合、これだと「電話」クラス用のデストラクタが呼び出されてしまいます。

このような不都合を避けるため、「電話」クラスのデストラクタを仮想関数にする必要があるのです。そうするとデストラクタの「連鎖」が起こり、「電話」クラス型のポインタを通して delete した場合でもサブクラス側のデストラクタからスーパークラス側のデストラクタへと順番に呼び出されていきます。これはちょうどコンストラクタの連鎖と逆の動作です。コンストラクタはスーパークラス側からサブクラス側へ向かって呼び出されていきます。

賢明な方なら「すべてのメソッドが仮想関数になればいいのに」と思うかもしれません。しかし C++ では virtual キーワードをつけたメソッドだけが仮想関数になります。Java では何もつけなくてもすべてのメソッドが仮想関数です。

これは、実行時の速度効率を考慮しているのです。仮想関数の呼び出しは「ダイナミックバインディング」と言って、実行時にはじめてどのクラスのメソッドを呼ぶのかが決定されるため、通常のメソッド呼び出しより時間的なコストがかかるのです(ほんの少しだけですが)。速度を犠牲にしたくないプログラマのために、C++ では仮想関数にするかどうかが細かく選べるようになっているというわけです。

もっと!
もっと詳しく知りたい人は次の用語について調べると良いでしょう:
  • Java におけるポリモーフィズムと動的メソッド検索

(「オブジェクト指向のはなし」は1999年2月から2000年4月にかけて作成されたコンテンツです。)