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

 こんにちは。今回は、前回の続きです。内容的にはほとんど同じですが、純粋仮想関数というのがでてきます。
 前回までにやってきたことは、基底クラスからいろいろなクラスを派生させ、似て非なるデータをうまく格納するオブジェクトを作り、それらを基底クラスのポインタで統一的に扱うと便利であるということでした。その際、基底クラスのポインタで表されているオブジェクトのメンバ関数を正しく呼ぶためには、そのメンバ関数を仮想関数にしなければならないのでした。
 これは(C++における)「オブジェクト指向」と呼ばれるスタイルのプログラミング方法なのです。今回は「入門15」「入門16」「入門17」で考えた「猫」たちをもう一度使って、考えることにしましょう。

 まず、ちょっと、復習します。Neko(猫)という基本のクラスがあり、そこから、SalaryNeko(サラリー猫)とSisankaNeko(資産家猫)を派生させました。「サラリー猫」はデータとして、「月給」をもっていて、その「年収」は「月給」の12倍としました。「資産家猫」のデータは「資産」でその「年収」は「資産」の2パーセント(利子ですね)として与えられるのでした。また、SalaryNekoWithBonus(サラリー猫・ウイズ・ボーナス)という「猫」が「サラリー猫」から派生していて、その「年収」は「月給」の12倍に「ボーナス」を足すのでした。
 さて、これらを全部「猫」のポインタで示して、get_nensyu()をうまく呼び出せれば、それぞれ、正しい年収が計算されるはずです。そのためには、get_nensyu()を仮想関数にすればよさそうです。では、どうすればよいかと言うと、前回のようにやるには基本になるクラスの「猫」のget_nensyu()の宣言の前にvirtualと書けばよいのでした。楽勝、、、と思いきや、「猫」にはget_nensyu()なんて関数はないのです。
 「猫」は「サラリー猫」、「サラリー猫・ウイズ・ボーナス」や「資産家猫」の元になるクラスで、ポインタはこのクラスのものを使うのがふさわしいはずです。しかし、「猫」は基本的すぎて、年収に関する情報は持っていないのです。
このような場合、例えば、get_nensyu()を純粋仮想関数というものにして「猫」に置くという方法も使われます。ここで、置かれる関数get_nensyu()は実際には「猫」には必要のない関数です。こういうときには、

//元祖「猫」
class Neko
{
    string name; //名前
public:
    Neko(string); //コンストラクタ
    void naku() const;  //鳴く関数
    //純粋仮想関数
    virtual int get_nensyu() const = 0;

};

Neko::Neko(string s) : name(s){}

void Neko::naku() const{
    cout << "にゃあ。俺様は" << name << "だ。" << endl;
}

とします。
 「virtual int get_nensyu() const = 0;」の「virtual」が「仮想関数であること」を意味し、「= 0」が「純粋であること」を表しています。「純粋」とは「このクラスでは使わない、派生クラスで使う」と言う意味です。constは、この関数がオブジェクトのデータを変更しないので付けてあるのでした。これは「(純粋)仮想関数であること」とは、何も関係ありません。(つまり、constの(純粋)仮想関数も、constでない(純粋)仮想関数も同様に定義できます。違いは、単に、constがあるかないかだけです。)
 このクラスで使わないので、定義は書きません。このような宣言をすると派生クラスの同名の関数がすべて仮想関数になるのは、前回と同じです。 ひとつ注意すべき事は、このように純粋仮想関数を宣言すると、そのクラスは「抽象クラス」と呼ばれるクラスになることです。そして、抽象クラスのオブジェクト(インスタンス)はつくれないのです。つまり、我々の例で言うと、改造したNekoは純粋仮想関数をメンバに持つので、抽象クラスです。抽象クラスなので、例えば、

    Neko dora("ボス");

のように、オブジェクトを作れなくなったのです。(本当はこういうことをされると、クラスNekoの利用者は困ります。でも、今日は練習なので、この路線でいくことにしましょう。Nekoに「純粋」でない(しかし使われない)仮想関数を無理につくるという路線もありますが。)
それでは、サンプルをつくってみましょう。

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

//元祖「猫」
class Neko
{
    string name; //名前
public:
    Neko(string); //コンストラクタ
    void naku() const;
    //純粋仮想関数
    virtual int get_nensyu() const = 0;

};

//「猫」の派生クラス「サラリー猫」
class SalaryNeko : public Neko
{
    int gekkyu; //月給
public:
    SalaryNeko(string, int); //コンストラクタ
    //年収を戻す関数。年収は月給の12倍とする。
    int get_nensyu() const { return gekkyu * 12; }
    //月給を1万円増やす関数
    void syoukyu() { gekkyu++; }
    //新しく付け加える「月給の値を戻す関数」
    int get_gekkyu() const{ return gekkyu; }
};

//「サラリー猫」の派生クラス「サラリー猫・ウイズ・ボーナス」。もう英語むちゃくちゃ。
class SalaryNekoWithBonus :public SalaryNeko
{
    int bonus;  //月給の何ヶ月分かを表す
public:
    //コンストラクタ
    //第1引数が名前、第2引数が月給、第3引数がボーナスを表す
    SalaryNekoWithBonus(string, int, int);
    //年収(月給+ボーナス)を戻す関数
    int get_nensyu() const{return get_gekkyu() * (12 + bonus);}
};

//「猫」の派生クラス「資産家猫」
class SisankaNeko : public Neko
{
    int sisan;  //資産、万円単位
public:
    SisankaNeko(string, int);     //コンストラクタ
    //年収を戻す関数。年収は資産の利子のみで、利率は2%とする。
    int get_nensyu() const{ return sisan * 2 / 100; }
};

Neko::Neko(string s) : name(s){}

void Neko::naku() const{
    cout << "にゃあ。俺様は" << name << "だ。" << endl;
}

SalaryNeko::SalaryNeko(string s, int x) : Neko(s), gekkyu(x){}  //コンストラクタ

SalaryNekoWithBonus::SalaryNekoWithBonus(string s, int g, int b) : SalaryNeko(s, g), bonus(b){}

SisankaNeko::SisankaNeko(string n, int s) : Neko(n), sisan(s){}

int main()
{
    int syurui;     //Nekoの種類を一時格納する場所
    string name;  //名前の一時格納場所
    int gekkyu;    //月給の一時格納場所、簡単のため今回は昇給はさせません。
    int bonus;     //ボーナスが月給の何ヶ月分かを一時的に格納。
    int sisan;      //資産の一時格納場所
    
    Neko *cat[5];   //5猫(ごにんと読む)分のNekoポインタ

    //データの入力
    cout << "5猫のデータを入力します。" << endl;
    for(int i = 0; i < 5; i++){ 
        cout << "職業を決めてください。" << endl;
        cout << "1 サラリー猫(ボーナスなし) 2 ボーナスありのサラリー猫 3 資産家猫"<<endl;
        cout << "(半角入力ですぜ!)" << endl;
        cin >> syurui;
        cout << "名前を入力してください:";
        cin >> name; 
        switch(syurui){
            case 1:
                cout << "月給(万円単位で整数)を入力してください。" << endl;
                cin >> gekkyu;
                cat[i] = new SalaryNeko(name, gekkyu);
                break;
            case 2:
                cout<<"月給(万円単位で整数)を入力してください。"<<endl;
                cin >> gekkyu;
                cout << "ボーナスは月給の何ヶ月分か入力してください。(整数で)" << endl;
                cin >> bonus;
                cat[i] = new SalaryNekoWithBonus(name, gekkyu, bonus);
                break;
            case 3:
                cout << "資産(万円単位で整数)を入力してください。" << endl;
                cin >> sisan;
                cat[i] = new SisankaNeko(name, sisan);
                break;
        }
    }

    cout << "それでは各自自己紹介します。よろしいですか?"<<endl;
    cout << "1 はい 2 いいえ" << endl;
    cin >> syurui;
    //ユーザが1以外の整数を入力したら終了
    if(syurui  != 1) return 0; //mainの中での「return 0;」はmainを終了させる

    for(int i = 0; i < 5; i++){
        cat[i]->naku();
        cout<<"年収:"<<cat[i]->get_nensyu()<<"万円"<<endl;
    }

    //後始末
    for(int i = 0; i < 5; i++)
        delete cat[i];

    cout<<"おしまい"<<endl;
}


(出力部分のみ)

 こんなものでしょうか。相変わらず入力のところがもたもたしていますが、仕方ないでしょう。
 くどいですが、「オブジェクト指向プログラミング」とは、クラス、継承、仮想関数を(うまく)使うプログラミングのことです。私自身の初心者の頃の感想は「仮想関数は便利そうだけど、大騒ぎするほどのことはないんじゃないだろうか」というものでした。みなさんはどう思いましたか。実は、私は今でも、仮想関数より、クラスの存在そのものの方にお世話になることが多い(つまり、プログラムの部品化です)ようです。もちろん、仮想関数は大事な仕組みで、また、便利であることは間違いありません。気張らずに、自分の思った通りにプログラムをしてみてください。
 最後に用語を1つ。「基底クラスのポインタで派生クラスのオブジェクトを扱い、仮想関数でオブジェクトに関する処理を正しく呼び出すしくみ」をポリモーフィズとかポリモルフィズムとか多態性とか多相性とか、まあ、そんな風に言います。
 今回の話はこれでおしまいだったのですが、かなりの方から、ファイルへの書き込み方法を質問されました。ここで、ちょっとだけお話します。
 画面への書き出しは普通、「cout << "....."」でやっていました。ここで、coutは画面を表し、「<<」はそこへ「押し込む」感じを表しているのです。同様にファイルを表すものがあれば、そこへ「押し込んで」書き込むことができます。ファイル名を例えば、test.txtとすれば、それは、

    ofstream f("test.txt");

などと、書かれます。ここで、fがファイルを表すオブジェクトになるのですが、私が勝手にfと名付けただけですから、別の名でもかまいません。ただし、このオブジェクトを使うときには、fstreamをインクルードする必要があります。これを使って、猫たちの年収を順にファイルtest.txtに書き出すには、

#include <iostream>
#include <string>
#include <fstream> //ファイル操作に必要
.......
.......
int main(){
    ofstream f("test.txt");
    .......
    for(int i = 0; i < 5; i++){
        cat[i]->naku();
        f << "年収:" << cat[i]->get_nensyu() << "万円" << endl;
    }
    .......
}

とすればよいのです。このプログラムを実行すると、自己紹介は画面に現れますが、年収の表示はなくなります。それはファイルtest.txtに書かれているのです。このファイルは(プログラムの)実行ファイルと同じディレクトリに作られていると思います。エディタ等で開いて読んでみてください。また、自己紹介もファイルに書く場合はnaku()を改造するか、もっとおすすめは、ファイルに自己紹介を書く新しい関数をつくるなどする必要があります。
 これで、「入門」は終わりにしようと思っていました。書き残したことは、まだまだ、ありますが、全てを網羅的に書くことがこの「講座」の目的ではないと前に書きましたよね。えへへ。(とても大変そうなんで、許してください。)あと、何を書き残したのか、次へのステップはどうすればよいか、などを書いて、、、と思っていたのですが、一方で、どうしても気になることがありました。それは、「コピーコンストラクタ」「代入演算子」という話題です。これも、「みなさん勉強してくださいね。」と言って終わりにしようかと、ずっっっっっっっっっっっっと悩んでいました。全てを説明するには、「入門」数回分は必要ですし、たぶん、大部分の「読者」は退屈すると思ったからです。でも、無視するのも、無責任と思えるので、あと2回延長して、この話をすることにしました。まだ自分は「初心者」だと思う人は今回で「入門」は卒業と思ってください。あとは、自分の思いのままにプログラムを書いてみてください。もし、今までの知識では不十分だと思うようになったら(すぐ思うでしょう、えっ、もう思っています?)、何か自分にあった教科書を読んでみてください。もう少し、本格的な知識もほしいと思う人は、あと少しだけつきあってください。
目次のページ
前のページ 後のページ