クラスの継承

クラスの継承

クラスという概念の導入によってプログラムの設計方法というか、考え方が変わるといいましたが、その一番かなめの部分がこの継承です。
inheritance と書きますが、これは日本語で相続・継承・財産などの意味があります。
クラスを元にして新しいクラスを作ることを継承といいます。

player クラスというものを考えてみますしょう。
player クラスを新しく作っても良いのですが、じつはplayer クラスはpartycharaクラスをすべて含んでいると考えられます。
だったら、partychara クラスに player クラスで必要な機能を追加してやればはじめから作るよりは簡単でしょう。
では、実際に partychara クラスから player クラスを作ってみましょう。

player クラスで新しく必要なのは selectcommand(); という関数だとします。

この場合に partychara クラスを基本クラス、player クラスを派生クラスといいます。

派生クラスの作成例 ----------------------
 

 #include <iostream.h>
 class partychara{
 private:
 static int znum;
 char *name;
 int age;
 int hp;
 int trib;
 public:
 int getznum(){ return znum;};
 partychara(char *nameis, int ageis, int hpis, int tribis);
 };
 
 partychara::partychara(char *nameis, int ageis, int hpis, int tribis)
 {
  name = nameis;
  age = ageis;
  hp = hpis;
  trib = tribis;
  cout << "コンストラクタでごじます" << endl;
  znum +=1;
  cout << getznum() << endl;
 }
 
 //----- player クラスを派生させる
 class player : public partychara{
 public:
 void selectcommand();
 player(char *nameis, int ageis, int hpis, int tribis);
 
 };
 
 //----- player クラスのコンストラクタの定義 実際は partychara のコンストラクタを呼び出している
 player::player(char *nameis, int ageis, int hpis, int tribis) : partychara(nameis, ageis, hpis,  tribis)
 {
 //----- ここにplayer クラス特有の初期化処理を書く
 
 }
 
 void player::selectcommand()
 {
  cout << "プレイヤはコマンドを選択した\n"; //----- player クラスの関数
 }
 
 int partychara::znum = 1234;
 
 void main()
 {
  player ps1("sukenosuke",1,2,3); //----- ps1という player オブジェクトの作成
 
  player pz[3]={ //----- pz という player の配列を作成
  player ("ichiro",1,2,3),
  player ("jiro",1,2,3),
  player ("saburo",1,2,3),
 };
 
 ps1.selectcommand(); //----- ps1 の新しい関数を呼ぶ
 
 }
 

これが partycharaクラス から player クラスを派生させたコードです。
一番上にあるのが partychara クラスとそのメンバ関数の定義、下の

 //----- player クラスを派生させる
 class player : public partychara{
 public:
 void selectcommand();
 player(char *nameis, int ageis, int hpis, int tribis);
 };

の部分が派生させた player クラスです。
宣言の部分は

class player : public partychara{

つまり

class 派生クラス名 : public 基本クラス名 {
}

という構造になっています。
みると、なるほど、と思いますが基本クラス名の前の public とはどういう意味でしょうか?
これは基本クラスをどのように扱うか、という意味です。
ここには

public
protected
private

と書くことができます。
これでなにが変わるかというと、派生したクラスから基本クラスのメンバがpublicになるか、protected になるか、private になるか、が変わります。

基本クラスのメンバと、基本クラスの指定の関係を表にすると
基本クラスのメンバ
基本クラスの扱い
派生クラスの扱い
public
protected
private
 public public
protected
アクセスできない
public
protected
private
 protected protected
protected
アクセスできない
public
protected
private
 private private
private
アクセスできない

つまり基本クラスの前につけるキーワードによって、派生クラスから呼び出せる基本クラスのメンバ変数やメンバ関数の扱いが制限される、ということになります。
普通は public を指定します。


そして下が、 player クラスのコンストラクタの宣言、

player(char *nameis, int ageis, int hpis, int tribis);

これが player クラスのコンストラクタの定義です。

 //----- player クラスのコンストラクタの定義 実際は partychara のコンストラクタを呼び出している
 player::player(char *nameis, int ageis, int hpis, int tribis) : partychara(nameis, ageis, hpis,  tribis)
 {
 //----- ここにplayer クラス特有の初期化処理を書く
 }

コンストラクタを使って派生クラスのオブジェクトを作る場合はどうすべきでしょうか?
基本クラスの関数をアクセスできるからと

partychara p1();

とやったのでは当然できるのは partychara クラスのオブジェクトですね。
ですから、基本クラスのコンストラクタがあろうとなかろうと、派生クラスでコンストラクタを使用する場合は、新しく定義しなければなりません。

しかし、基本クラスとやることが同じなら、新しく定義するのはスマートではありませんね。
そこで上のように player クラスのコンストラクタから partychara クラスのコンストラクタを呼び出すことができるようになっています。

player::player(char *nameis, int ageis, int hpis, int tribis) : partychara(nameis, ageis, hpis, tribis)

これはplayer クラスのコンストラクタが受け取った引数を使って partychara コンストラクタを呼び出す、という事をあらわしています。
その上で player クラスのコンストラクタ内で必要な初期化(たとえば player だけが使える魔法陣を初期化する)が必要なら player クラスのコンストラクタの中にそれを定義します。

 //----- player クラスのコンストラクタの定義 実際は partychara のコンストラクタを呼び出している
 player::player(char *nameis, int ageis, int hpis, int tribis) : partychara(nameis, ageis, hpis, tribis)
 {
 //----- ここにplayer クラス特有の初期化処理を書く
 mahoujin = 3; // 魔法陣を3回呼び出せるという設定
 }

という様に書きます。
これで partychara オブジェクトでは mahoujinが0ですが、player オブジェクトでは
3という値がセットされることになりました。


クラスの継承でコンストラクタとともに気をつけなければいけないのはポインタについてです。
以下のような場合を考えてみます。

partychara クラスに gohome(); というメンバ関数があるとしましょう。

partychara クラスの gohome(); を実行すると、そのキャラクタはパーティを離れて自分の故郷に帰ってしまう、ということにします。
実際にはキャラクタの位置の変数があってそこが故郷の座標になる、みたいな処理をすることになるのでしょう。

ところが partychara を継承して作った player クラスの gohome(); は、ゲームの中断準備処理をするとします。
するとこうなります。
 

 void main()
 {
  partychara p1("魔法使い",1,2,3); //----- p1という player オブジェクトの作成
  player ps1("勇者",1,2,3); //----- ps1という player オブジェクトの作成
 
  p1.gohome(); // p1 は故郷に帰る
 
  ps1.gohome(); // ゲームを終了するために各種パラメータを保存する
 
 }
 
ここまではOKです。
これをポインタを使って以下のようにすると
 
 void main()
 {
  partychara *ppp;
  partychara p1("魔法使い",1,2,3); //----- p1という player オブジェクトの作成
  player ps1("勇者",1,2,3); //----- ps1という player オブジェクトの作成
 
  ppp = &p1; // ポインタに partychara のアドレスをセット
 
  ppp->gohome(); // p1 は故郷に帰る
 
  ppp = &ps1; // ポインタにplayer のアドレスをセット
 
  ppp->gohome(); // ゲームを終了するために各種パラメータを保存する
 // はずなのに、ps1は故郷に帰ってしまう。
 }
 
これを確認するための実際のコードは以下の通りです。
 
 #include < iostream.h>

 class partychara{
 protected:
 char *name;

 public:
 partychara(char *nameis);
 void gohome(); // partychara クラスの gohome
 };
 
 void partychara::gohome()
 {
  cout << name << " は故郷に帰った・・・さようなら" << endl;
 }
 
 partychara::partychara(char *nameis)
 {
  name = nameis;
 }
 
 //----- player クラスを派生させる
 class player : public partychara{
 public:
 player(char *nameis);
 void gohome(); // player クラスの gohome
 };
 
 player::player(char *nameis) : partychara(name)
 { 
  name = nameis;
 
 }
 
 void player::gohome()
 {
  cout << name << " は帰り支度をした。\nゲームを終了しますか?" << endl;
 }
 
 void main()
 {
  partychara *zz;
 
  partychara p1("魔法使い"); //----- ps1という player オブジェクトの作成
  player ps1("勇者"); //----- ps1という player オブジェクトの作成
 
  zz = &p1; // ポインタに partychara p1 のアドレスをセットする
  zz->gohome(); // partychara のgohome();・・・・・・・・・・・・・OK
  zz = &ps1; // ポインタに player ps1 のアドレスをセットする
 
  zz->gohome(); // player の gohome() を呼んだつもりだが・・・・・ NG
 
  p1.gohome(); // partychara の gohome()・・・・・・・・・・・・・OK
  ps1.gohome(); // player の gohome()・・・・・・・・・・・・・・・OK
 
 }
 

ここでためしに player::gohome() ではなく、 player::gohome2() を作ってそちらを
呼び出してみましょう。
エラーになりますね。
ということは、gohome() は、partychara のgohome() が否応なく呼び出されるという
ことです。
これを防ぐには、class partychara の中の

void gohome(); // partychara クラスの gohome

に Virtual キーワードをつけて

Virtual void gohome(); // partychara クラスの gohome

とします。
試しにこれでビルド、実行すると、ちゃんと player::gohome() が呼び出されます。

これが仮想関数というしくみです。
まとめると

クラスのメンバ関数を Virtual キーワードを使って宣言しておくと、継承したクラスに同じ名前の関数を定義すると、ポインタ経由でアクセスした場合、そちらが呼ばれる。

ということです。
ちょっとややこしいですね。
おまけにどういう場面でこのしくみが有効なのか、見当も付きませんね。
初心者のうちは確かにそう感じるでしょう。
しかし、このオブジェクトのポインタを使ってメンバ関数を呼び出す、というのは慣れると普通に使います。
というか、そのように使った方が何倍も便利なのです。

なぜ便利かというと、同じ関数を使えるからと言うと説明になりませんね。
クラスの一番便利な所は、同じようなものがいくつもあるもの、を扱うときです。
ゲームで言うと、敵キャラ、アイテム、種族、などでしょう。ビジネスソフトでは社員、在庫、取扱商品、などでしょうか。

ゲームキャラで言うと、アイテムというクラスをまず、作ります。
アイテムを継承してポーションと、武器を作ります。
本当はそれぞれいろいろ種類を作りますが、ここでは体力回復のポーションとロングソードを作ります。

キャラクタが持ち物を20個持っているとすると、プレイヤは20個のうちから一つ選んで使います。

その時に、選んだオブジェクトをポインタにセットします。
そのポインタは、自分ではポーションがセットされているのかロングソードなのかわかりませんね。

ポーションとロングソードを使ったときのメンバ関数名が違った場合は
 

 item *p;
 p = SelectItem(); // プレイヤが選択したアイテムのオブジェクトをポインタにセット
 
 case (type){
 sord : p->Use_sord(); // ロングソードを使う
 break;
 portion : p=Use_portion(); // ポーションを使う
 break;
 
 }
 

のようにいちいち余計な判断をしなければなりません。

アイテム使用のメンバ関数の名前が同じなら、ただ単に

p->Use();

の1行ですむわけですね。
この時点では p にどのオブジェクトのアドレスがセットされていようが
正しい Use() が自動的に呼び出されます。

宿屋でポーションをつかうと
「勇者のMPが満タンになった!!」

宿屋でロングソードを使うと、
「お客さん、素振りは外でやってくだせえ!!」

と表示することが出来ます。

どうですか、ちょっと便利さを感じましたか?

ついでにちょっと考えて下さい。
この場合、アイテムの Use() が定義されている必要があります。
しかし、実際にゲームで使うのはポーションだったり、武器だったり魔法のローブだったりします。
つまり、アイテム、というオブジェクトは作るひつようが無いわけです。
それなのに、 Item.Use() という名にもない関数を定義(つまり書く)のは無駄だと思いませんか?
無駄無駄、無駄ですねー。
そこで、 Use() の名前だけを宣言して中味の定義を省略してしまいます。
そのやり方は、

Virtual Void Use() = 0;

と書きます。
この実体を持たない関数を純粋仮想関数といいます。
純粋仮想関数をもつクラスは、オブジェクトを作ろうとするとエラーになります。
もしオブジェクトが作れると実体の無いメンバ関数を持つことになってしまい、おかしなことになりますね。
このように純粋仮想関数をもち、インスタンスを作ることができないクラスを抽象クラスといいます。


上の例では、 item と言う抽象クラスを作ることでプログラムをすっきり書くことが出来ました。

もちろん、抽象クラスなどというややこしいものを使わずに素直に実体化されることの無い、Item.Use() を書いても全然OKです。

思うに、C++コンパイラを設計するくらいの人は、絶対に使われないコードを書くのがいやなのだと思いますね。
カッコ悪いというか、美しくないというか。

絶対に使われないコードを書くより、抽象クラスを使った方が美しいコードだ、と感じるようになったら、初級プログラマ卒業準備完了です。

これまでのクラスの説明は、コンソールプログラムに関してでした。
コンソールプログラムは余計なものが無いので説明も簡潔でした。
クラスを作るのも書き方さえ覚えればすぐに作ることができました。

次にするのはウィンドウズのアプリケーションを作ることです。
これは、VC++の機能を使うことになります。

VC++は親切な部分もありますが、C++を理解していないと何をしているかわからない部分もあります。

MFCを使ったウィンドウズアプリを作るときにどうやってクラスを扱うか、をこれから説明します。