こんにちは。突然、寒くなりましたね。(私は小田原在住です。他の地方の方はどうなんでしょう?)さて、最近は継承の基礎を学んでいるわけですが、もう少し遊んでみましょう。
クラス サラリー猫・ウイズ・ボーナス : クラス サラリー猫
データメンバ:ボーナス(月給の何ヶ月分かで表すことにする)
メンバ関数 :年収の関数(月給とボーナスを足して戻す)
//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;
}
などとなります。これは、クラスの宣言と関数の定義をそれぞれまとめた書き方です。
なんだか、社員名簿のプログラムの様になってきましたね。猫はもっと自由なはずなのに。もし、前に書いたゲームもどきが好きな方は、そちらで継承の実験をしてみてください。今日はこの辺で。