C++によるプログラミング入門17
さらに継承

 こんにちは。突然、寒くなりましたね。(私は小田原在住です。他の地方の方はどうなんでしょう?)さて、最近は継承の基礎を学んでいるわけですが、もう少し遊んでみましょう。


 前々回に給料をもらっている「サラリー猫」、前回は利子生活者の「資産家猫」をつくりました。作り方は簡単で、「猫」から派生させたのでした。このとき、「猫」を基底クラスというのでした。(基本クラスという人もいます。)もう少し、練習したい人は、無収の猫「浮浪猫」なんかをつくってみてください。
 こんな風に基本的なクラスをつくっておけば、どんどん派生クラスがつくれるので、C++は便利なのです。(くどいですが、本当に役立つプログラムにするためには、基本となるクラスをもう少し、きちんとつくる必要があります。ほんとにくどいですね。)
 さて、実は、派生クラスからさらに派生クラスもつくれます。(この場合は元の「派生クラス」は新しい派生クラスの基底クラスとよばれます。めんどいですね。)たとえば、前々回の「サラリー猫」の年収は月給の12倍で、ボーナスなしでした。(私はOD(オーバードクター)として、なが〜いことそういう生活をしてきました。)ここで、ボーナスをもらえる「サラリー猫・ウイズ・ボーナス」をつくりたいとします。どうしますか?一つの方法は「猫」から新しい派生クラスをつくるという方法があります。しかし、「サラリー猫・ウイズ・ボーナス」は、「サラリー猫」に似ているので、「サラリー猫・ウイズ・ボーナス」を「サラリー猫」から派生させることができます。実際にやってみましょう。

 クラス サラリー猫・ウイズ・ボーナス : クラス サラリー猫
    データメンバ:ボーナス(月給の何ヶ月分かで表すことにする)
    メンバ関数 :年収の関数(月給とボーナスを足して戻す)

とでもしてみましょう。ただ、ここでちょっと困ったことがおこります。というのは、「サラリー猫・ウイズ・ボーナス」の年収を計算するのに、月給の値が必要です。しかし、それは「サラリー猫」のプライベート(private)メンバなので、たとえその派生クラスでも、使うことはできないのです。
 えーと、意味はわかりましたか。gekkyuは「サラリー猫」のメンバなので当然その派生クラスである「サラリー猫・ウイズ・ボーナス」のメンバでもあります。しかし、「サラリー猫・ウイズ・ボーナス」はgekkyuを直接扱うことができないのです。なんと不便な、と思うかもしれませんが、そうなっているのです。
 ああめんどくさい!と、思ってgekkyuをパブリックにしてしまう方法もあります。そうすればどこからでもアクセスできるからです。C++をはじめた頃の私はそうでした。実際そのように書いてある初心者向けの教科書もあります。しかし、くどくど説明してきたように、これはよくない方法です(断言しておきましょう^^;)。そのうち、クラス(C++の宝)をつくる気がなくなってゆくからです。(白状すると、そのような道を通って、一時期Cへ戻ろうと考えたこともありました。まあ、結局、C++が気に入ってしまったのですが。)
 この問題を解決するために、他のクラスからでも「サラリー猫」の月給がわかるようなパブリックな関数を増設しましょう。それは、単に「月給を戻す関数」とし、名前は「get_gekkyu()」とします。変な名ですが、誤解の無いように。「働いて月給を取ってくる関数」ではなく、単に「return gekkyu;」とするだけですよ。
この関数をつくっておきさえすれば、gekkyuの必要な場所にかわりにget_gekkyu()と書けばよい、となるのです。

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

//元祖「猫」
class Neko
{
    string name; //名前
public:
    Neko(string); //コンストラクタ
    void naku() const;
};

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

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

//「猫」の派生クラス「サラリー猫」
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; }
};

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

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

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

//前回のmain()とほぼ同じだが、省ける中カッコを省いてみた。
int main()
{
    string name;  //名前の一時格納場所
    int gekkyu;    //月給
    int bonus;     //ボーナスが月給の何ヶ月分か

    cout << "サラリー猫・ウイズ・ボーナスをメモリ上に生成します。\n名前を決めて入力してください。" << endl;
    cin >> name;
    cout << "月給を決めて入力してください。(1万円単位)" << endl;
    cout << "(数字は半角で入力してください。)" << endl;
    cin >> gekkyu;
    cout << "ボーナスは月給の何ヶ月分か、入力してください。" << endl;
    cin >> bonus;
    SalaryNekoWithBonus dora(name, gekkyu, bonus);  //サラリー猫・ウイズ・ボーナスの生成
    //ループ。抜けるにはユーザが1、2、3以外の数字を入力すればよいようにする。
    while(1){
        int ans;
        cout << "どうしますか?" << endl;
        cout << "1 鳴かす 2 年収を表示 3 昇給 4 やめる"<<endl;
        cin >> ans;
        if(ans == 1)
            dora.naku();
        else if(ans == 2)
            cout << "年収は現在" << dora.get_nensyu() << "です。" << endl;
        else if(ans == 3){
            dora.syoukyu();
            cout << "1万円昇給しました。" << endl;
        }
        else
            break;
        //見やすさのための改行
        cout << endl;
    }
    cout << "おしまい" << endl;
}

まあ、こんなもんでしょうか。
C++のひとつの方針は「書き直し」を極力減らし、必要なら「書き足す」ということです。つまり、はじめに「サラリー猫」をつくったときには、猫にボーナスをやろうなんて考えてもいなかったのです。ところが、時勢の変化で、猫にもボーナスをやらないと会社が立ち行かなくなってきた、さあ、どうしましょうというわけです。気の短い人は「サラリー猫」を直接書き直すでしょう。しかし、また、いつボーナスなしの猫が必要になるかわかりませんね。できれば残しておきたいです。
それで、次に考えられるのは、はじめに言ったように、「サラリー猫」をコピー・アンド・ペーストして、書き直すという方法です。私はコピー・アンド・ペーストの方法をはじめて知ったときの感動は忘れません。「世の中さ、進歩してんだな〜。」と子供心(当時大学院生でした)に思いました。今でも使っています。
しかし、実は、これもそんなに良い方法ではありません。長いプログラム(自分にとって長いプログラム)の最大の敵は「似て非なるコード」です。似て非なるコードがちりばめられたプログラムは最低と言ってよいでしょう。たとえば、もし、そのコードに間違いがあった場合プログラムのすみずみまで探し回って、直さないといけません。えっ、あなたは間違えない人ですか。それはすごいですが、たとえ間違いがなかったとしても、後で、変更する必要が出てくるかもしれないですよね。いや、必ずでてくるはずです。(なんちゃって。)そのときは、、、やはり大変です。
それに、そもそも、コピー・アンド・ペーストを繰り返して、少しづつ書き直していくと、間違いを起こしやすくもなります。(この部分をコピーして、、、あとで、xをyに書き換えて、、、なんてやっていて、xとyの書き換えをつい忘れる、、、なんてやってしまうのは、私だけでしょうか?^^;)私はいつも

 コピー・アンド・ペーストはバグ地獄への入り口

と自分に言い聞かせています。と、なると、最後に残るのが継承、派生クラスの方法なのです。

  ここまで読んですかっとした人は今日はもうやめてもよいと思います。ゆっくり休んでください。でも、ここまで読んで、「いや...」と思った人がいるかもしれません。「書き換えはしたくないと言いながら、サラリー猫でget_gekkyu()という関数を増やしている。これは、サラリー猫の書き換えじゃんか。」その通りです。これは関数を足したのであって、何かを削ったのではないので、それほど罪は重くないだろう、という言い訳もあります。何かを削る場合は、その何かが他で使われていないか、徹底的に探して、それも書き直して、、、となるので、大変なのです。しかし、足す分にはあまり問題はないでしょう。
 とは言え、「サラリー猫」の基本設計が悪かったとは言えます。 これは難しい問題にもつながっています。繰り返しますが、C++の基本方針の一つは「書き直しをなるべくなくす」ということですが、そのためには、将来の別の使われ方をある程度予測しながら、クラスを設計しなければならないということになります。これは、偉い人たちも難しいと告白しています。まあ、当然ですね。
 しかし、少しタネをあかすと、上の例は簡単な例です。非公開(private/プライベート)なデータを読み出したり、変更したりすることはいつもあり得ます。そのような予想なら、たぶん、できるでしょう。
 非公開なデータを読み出す必要がある場合、多くの人は「get何々()」という関数をつくります。また、そのデータを変更(設定)する関数がほしいときは「set何々()」とします。例えば、サラリー猫には「昇給」というささやかな楽しみを表す関数がありますが、社長の一声で減給になったり、2階級特進になったりするかもしれません。派生クラスなどで、gekkyuを操作することが予想される場合は、以下のように、最初から「月給を設定する関数」をつけておいた方がよいわけです。

//「猫」の派生クラス「サラリー猫」
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; }
    //月給の値を設定する関数
    void set_gekkyu(int x){ gekkyu = x; }
  
};

ボーナス猫のボーナスについても同様ですが、それはみなさんにお任せします。

 少しごちゃごちゃしました。しかし、簡単に言えば、非公開(private)なデータを、派生クラスなどで操作したいなら、「get何々()」「set何々()」などという関数をつけておく必要があるということになります。


 コードの書き方についてちょっとコメントがあります。ここまで、コードを

   クラスの宣言

   そのクラスの関数の定義

   別のクラスの宣言

   そのクラスの関数の定義

   ・・・

のように書いてきました。もちろん、それでもよいのですが、クラスの宣言とそのクラスの関数の定義は別のファイルに置くのが一般的です。ただ、この講座では、ファイルの分割まで扱いません。そこで、「気持ち分割」として、

   クラスの宣言

   別のクラスの宣言

   ・・・

   クラスの関数の定義

   別のクラスの関数の定義

   ・・・

のように、クラスの宣言を前の方にまとめて書き、クラスの関数の定義をそのあとに書くようにしてみます。たとえば、今の例では、

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

///////////////////// クラスの宣言部 /////////////////////
//元祖「猫」
class Neko
{
    string name; //名前
public:
    Neko(string); //コンストラクタ
    void naku() const;
};

//「猫」の派生クラス「サラリー猫」
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);
    //年収(月給+ボーナス)を戻す関数
    //SaralyNekoと同じ名前の関数、同じ名前でもよいのです。
    int get_nensyu() const{return get_gekkyu() * (12 + bonus);}
};

///////////////////// 関数の宣言部 /////////////////////
//Nekoの関数の定義
Neko::Neko(string s) : name(s){}

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

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

//SalaryNekoWithBonusの関数の定義
SalaryNekoWithBonus::SalaryNekoWithBonus(string s, int g, int b) : SalaryNeko(s, g), bonus(b){}

///////////////////// main /////////////////////
int main()
{
    string name;  //名前の一時格納場所
    int gekkyu;    //月給
    int bonus;     //ボーナスが月給の何ヶ月分か

    cout << "サラリー猫・ウイズ・ボーナスをメモリ上に生成します。\n名前を決めて入力してください。" << endl;
    cin >> name;
    cout << "月給を決めて入力してください。(1万円単位)" << endl;
    cout << "(数字は半角で入力してください。)" << endl;
    cin >> gekkyu;
    cout << "ボーナスは月給の何ヶ月分か、入力してください。" << endl;
    cin >> bonus;
    SalaryNekoWithBonus dora(name, gekkyu, bonus);  //サラリー猫・ウイズ・ボーナスの生成
    //ループ。抜けるにはユーザが1、2、3以外の数字を入力すればよいようにする。
    while(1){
        int ans;
        cout << "どうしますか?" << endl;
        cout << "1 鳴かす 2 年収を表示 3 昇給 4 やめる"<<endl;
        cin >> ans;
        if(ans == 1)
            dora.naku();
        else if(ans == 2)
            cout << "年収は現在" << dora.get_nensyu() << "です。" << endl;
        else if(ans == 3){
            dora.syoukyu();
            cout << "1万円昇給しました。" << endl;
        }
        else
            break;
        //見やすさのための改行
        cout << endl;
    }
    cout << "おしまい" << endl;
}

などとなります。これは、クラスの宣言と関数の定義をそれぞれまとめた書き方です。


 継承(派生)をより健全に実行するためには、ある程度の「予測能力」が必要で、それは、難しい問題ではあるのです。しかしそれでも、継承がパワフルな方法であることはわかっていただけたのではないでしょうか?(また、最近(?)では、基底クラスを書き直すことも、テストさえしっかりしておけば(それほど?)ひどいことではないという動きもあります。2005年1月記)
じゃんじゃん派生クラスをつくって遊んでみてください。自分で実験するととても楽しいですよ。

 なんだか、社員名簿のプログラムの様になってきましたね。猫はもっと自由なはずなのに。もし、前に書いたゲームもどきが好きな方は、そちらで継承の実験をしてみてください。今日はこの辺で。


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