C++によるプログラミング入門22
仮想関数

 こんにちは。この「C++入門」は、7、8年前に書いたものを、2005年現在書き直しています。初心を忘れてはいけないですね。
 さて、今回は「仮想関数」です。いよいよ「オブジェクト指向プログラミング」最後の項目ですね。...で、とりあえず、ポインタの復習をしましょう。ポインタというのは、オブジェクトの場所(メモリ上の場所)を表す値(アドレス)格納する変数です。ポインタにオブジェクト(クラスのインスタンス)のアドレスを代入して、何かするのでした。オブジェクトは、newを使って生成することもできます。例えば、次のプログラムを見てください。

//hito_sample.cpp
#include <iostream>
using namespace std;

//人を表すクラス
class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    void Jikosyoukai();  //自己紹介関数
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

int main()
{
    Hito *p;       //Hitoオブジェクトへのポインタ。まだ、何も代入されていない。
    p = new Hito(10);  //パワーが10のHitoオブジェクトが生成され、そのアドレスがpに代入される。
    p->Jikosyoukai();   //pの指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    delete p;      //pの指し示すオブジェクトを破棄。
}

 このプログラムを実行すると、Hitoオブジェクトが一度自己紹介して終わります。(なんなんだ。^^;) 何をやっているかわからない例ですが、まあ、説明のための例と思って我慢してください。まず、クラスHitoを定義し、mainの中では、そのアドレスを格納するポインタpを定義し、Hitoオブジェクトを生成し、そのアドレスをpに代入します。これにより、pを使って、今生成したHitoオブジェクトを扱えるわけです。実際、Jikosyoukai(自己紹介)という関数を呼び出しています。
 なお、あとで利用するため、Hitoには、powerの値を設定するset_powerと、powerの値を戻すget_powerという関数を付けてありますが、ここでは利用していません。

 ところで、クラスには「継承」というものがありました。Hitoから派生クラスをつくることができます。そして、C++では、派生クラスのアドレスも基底クラスのポインタに代入できることになっています。つまり、今の例では、Hitoから何か派生させた場合、そのクラスのオブジェクトのアドレスをHitoのポインタに代入できるのです。例を見てみましょう。

//hito_sample2.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    void Jikosyoukai();
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数
    void Jikosyoukai();
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

int main()
{
    Hito *p1, *p2;       //Hitoオブジェクトへのポインタ。まだ、何も代入されていない。
    p1 = new Hito(10);  //パワーが10のHitoオブジェクトが生成され、そのアドレスがp1に代入される。
    p1->Jikosyoukai();   //p1の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    p2 = new Samurai(12);  //パワーが12のSamuraiオブジェクトが生成され、そのアドレスがp2に代入される。
    p2->Jikosyoukai();   //p2の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    delete p1;      //p1の指し示すオブジェクトを破棄。
    delete p2;      //p2の指し示すオブジェクトを破棄。実は問題あり。後述。
}

 上のプログラムを実行すると、どうなると思いますか?mainの前半部分は前のプログラムと同じですね。
 p2に関しては、Samuraiオブジェクトを生成して、そのアドレスを代入しています。ちょっと、待てよ!と思った人。するどいです。p2はHitoのポインタです。したがってHitoオブジェクトのアドレスを格納することになんの問題はありません。しかし、それだけでなく、Hitoの派生クラスであるSamuraiのオブジェクトのアドレスを代入することもできるのです。まあ、そのように作られているわけです。もちろん、

Samurai *p3;
p3 = new Samurai(15);

のように、SamuraiオブジェクトのアドレスをSamuraiのポインタに代入することは当然できますが、ここではわざと基底クラスのポインタを使って見せたのです。
 もう一度言いましょう。p2はHitoのポインタであるにもかかわらず(Hitoの派生クラスである)Samuraiのオブジェクトを指し示すことができるのです。ここで

p2->Jikosyoukai();

とすると、どうなるでしょう?この場合、p2はHitoのポインタなのでHitoのJikosyoukaiが呼び出されます。SamuraiはHitoの派生クラスなので、Samuraiオブジェクトは内部にHitoオブジェクトを持っているようなものでした。そこで、Samuraiのオブジェクトに対してもHitoの関数を呼び出すことができるのです。

 結局、実行すると次のようになります。

 賢明なる紳士淑女のみなさんは、この辺で何かやるんだろ、やるんなら引き延ばさずに、はやくやってくれと思っていることでしょう。そうです。ここで「仮想関数」の登場です。 Hitoのメンバ関数Jikosyoukai()の宣言を

    void Jikosyoukai();

から、

    virtual void Jikosyoukai();

に変えてみます。これにより、Jikosyokai()は仮想関数というものになります。仮想関数とはどういうものかというと、「オブジェクトが基底クラスのポインタで示されている場合にも、オブジェクトの型の関数が(正しく)呼び出される関数」です。つまり、上の例の後半では、SamuraiのオブジェクトがHitoのポインタで表されるわけですが、この場合にも、p->Jikosyokai()はHitoのでなくSamuraiの関数になるのです。

//hito_sample3.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai();  //仮想関数になる
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数
    void Jikosyoukai();  //基底クラスでvirtual宣言しているので、これも仮想関数になる
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

int main()
{
    Hito *p1, *p2;       //Hitoオブジェクトへのポインタ。まだ、何も代入されていない。
    p1 = new Hito(10);  //パワーが10のHitoオブジェクトが生成され、そのアドレスがp1に代入される。
    p1->Jikosyoukai();   //p1の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    p2 = new Samurai(12);  //パワーが12のSamuraiオブジェクトが生成され、そのアドレスがp2に代入される。
    p2->Jikosyoukai();   //p2の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    delete p1;      //p1の指し示すオブジェクトを破棄。
    delete p2;      //p2の指し示すオブジェクトを破棄。実は問題あり。後述。
}

 難しく考えないでください。仮想関数とは、上のような働きをするものなのです。なお、virtualは基底クラスの関数の前につければその派生クラスの同名のメンバ関数に影響が及ぶので、一度つければ良いことになっています。ただし、派生クラスにも付けて、かまいません。
 想像力のある人は「仮想関数のもつ可能性」がもう見えてしまったかもしれませんが、(私のような)普通の人にはまだピンとこないでしょう。ちょっとだけ言っておくと、これはたくさんの似て非なるデータがある時に有効なものなのです。次回に簡単な具体例をお見せできると思います。

 なお、ここでちょっと面倒な話をしなければなりません。上のほうで、「Samuraiオブジェクトの中にHitoオブジェクトがある」という話をしました。それでは、

    delete p2;

では、ちゃんとSamuraiオブジェクトは破棄されるでしょうか?答えは、いいえ、です。p2はHitoのポインタなので、上の命令では、Samuraiオブジェクトの中のHitoオブジェクトだけが破棄されることになります。今の場合、Samuraiは独自のデータを持っていないので、あまり問題にならないかもしれませんが、よいことではありません。
 そこで、p2がHitoのポインタであっても、正しく「p2が指し示すSamuraiオブジェクト」を破棄するようにしておかなければならないのです。そのためには、Hitoに仮想デストラクタというものを付けることになっています。仮想デストラクタは

virtual ~Hito(){}

のようなものです。virtualが付いているデストラクタだから仮想デストラクタであるわけです。ここで重要なことは「仮想デストラクタがある」ということです。そのデストラクタ自身がするべき仕事はないので、中カッコの中は空でよいのです。つまり、まとめると、Hitoは、

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    virtual ~Hito(){};    //仮想デストラクタ
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai();  //仮想関数になる
};

としておくべきなのです。こうすれば、「delete p2;」で、ただしくSamuraiオブジェクトが破棄されます。
 どういうときに仮想デストラクタが必要なのでしょうか。それは、hito_sample2.cppのように、「派生クラスのオブジェクトを基底クラスのポインタで扱い、あとでdeleteをする場合」です。しかし、これはなかなか面倒な「条件」ですね。一度書いたプログラムをあとで書き直す場合、間違いが起こりそうです。そこで、一般には、「クラスが仮想関数を持てば、仮想デストラクタを付けるべき」と言われています。その理由は、仮想関数を持つクラスは、hito_sample3.cppのHitoのような使い方をされると予想されるからです。(実は、hito_sample.cppやhito_sample2.cppは説明のための例で、一般的なプログラムの書き方ではないと思います。)初心者には嫌なところですが、このルールを守れば、とりあえずOKでしょう。

 最後に、上の例に「忍者」「町娘」をいれた例をつくってみましょう。

//hito_sample4.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    virtual ~Hito(){};    //仮想デストラクタ
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai();  //仮想関数になる
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数(これも仮想関数になる)
    void Jikosyoukai();  //基底クラスで同名の関数にvirtualを付けたので、ここにvirtualはいらない
};

//Hitoの派生クラスNinja
class Ninja : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Ninja(int x) : Hito(x){}
    void Jikosyoukai();
};

//Hitoの派生クラスMatimusume
class Matimusume : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Matimusume(int x) : Hito(x){}
    void Jikosyoukai();
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

void Ninja::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "拙者は忍者でござる。" << endl;
    cout << "拙者のパワーは" << get_power() << "でござる。" <<endl;
}

void Matimusume::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "あたいは江戸っ娘よ。" <<endl;
    cout << "あたいのパワーは" << get_power() << "よ。" <<endl;
}

int main()
{
    Hito One(10);        //人
    Samurai Two(12);  //さむらい
    Ninja Three(14);     //忍者
    Matimusume Four(16);  //町娘
    Hito *p;       //Hitoのインスタンスへのポインタ。まだ、何も代入されていない。

    p = &One;
    p->Jikosyoukai();
    p = &Two;
    p->Jikosyoukai();
    p = &Three;
    p->Jikosyoukai();
    p = &Four;
    p->Jikosyoukai();
}

 この例では、あえてnew/deleteを使わない方法にしました。たとえば、Oneがオブジェクトである場合、&OneがOneのアドレスを表すので、それをHitoのポインタに代入できるわけです。この場合でも、仮想関数の性質により、正しいJikosyoukaiが呼び出されることになります。これは、ポインタを使っているからなのです。

 上のプログラムでは、deleteが使われないので、(一見)仮想デストラクタは不要です。しかし、new/deleteを使うときは必要になるので、削除などしてはいけません。「クラスが仮想関数を持てば、仮想デストラクタを付けるべき」のルールに従いましょう。


目次のページ
前のページ 後のページ