プロジェクト演習III,V <インタラクティブ・ゲーム制作> プログラミングコース 第7回 継承の使い方:C++編
今日の内容 先週のコードで継承が出てきた 今週のOOPでも継承が出てきた じゃあやらねばなりますまい! C++での継承の書き方 メリットその1:差分プログラミング コンストラクタ、デストラクタの注意点 メリットその2:多態性(ポリモフィズム) オーバーライド、仮想関数
でも、理解できないものを 無理に使う必要は無い C++は懐の広い言語なので、全部の言語仕様を使い切らずに、理解の出来る範疇で開発してもよい というか、言語仕様を完璧に把握してる人なんて……いるのだろうか? 元々C++はC言語に付け足して出来ているので、様々な時代の概念が混在している マルチパラダイム言語と呼ばれたり 色んな考え方をまぜこぜにしてもOK
とはいえ 出来ることの幅が広がった方が、後々の選択肢が増えたり、楽できるのも事実 一気に取り込まず、別プロジェクトで 実験してから順次導入していこう
継承が便利な時:その1 差分プログラミング プラグちゃんに改造パーツを取り付けた、 グレートプラグちゃんを作りたい 基本構造はプラグちゃんのままなので、 その部分はうまいこと使い回したい でもコピペはヤダ!後でプラグちゃんを修正した時にコピペ先も直さないといけないから プラグちゃんを継承し、 グレートプラグちゃんを作る
C++での継承の書き方 // 継承したいクラス(基底クラス)のヘッダをインクルードする #include “PlugChan.h” // extendsキーワードではなく、コロン(:)を挟むだけ // public以外のキーワードも指定できるが、基本的にpublicのみ class GreatPlugChan : public PlugChan { // ここにGreatPlugChanとして付け足したいメンバを宣言 }; // cpp側は、普通にこのヘッダをインクルードして実装すればよい
コンストラクタはどうなる? 基本的には継承元 (基底クラス)のコンストラクタは勝手に 呼んでくれる 継承先(派生クラス)で新たにコンストラクタを作ってもよい ただし、引数付きのコンストラクタを使う場合は、継承先(派生クラス)の コンストラクタで明示的に呼ぶ必要がある // 基底クラスParentのコンストラクタが整数値を取る場合 #include “Parent.h” Child::Child(int hoge) : Parent(hoge) // これで呼べる { } // メンバ変数の初期化リストと同じような書き方なので、混同に注意!
デストラクタは? コンストラクタとは逆に、派生側から 順番に呼ばれていく ただし、継承されることが前提となっているクラスでは、デストラクタの宣言にvirtualを付けよう 付けておかないと、後々壮絶なメモリリークをかますおそれがあるので
差分プログラミングの他の例 既にあるクラスへの付け足しや書き換え 共通の処理や構造をまとめておき、 状況に合わせて必要なものを付け足す fk_Modelを継承して、ブロック専用モデルや 球専用モデルを作るなど(fkut_???Model) 共通の処理や構造をまとめておき、 状況に合わせて必要なものを付け足す fkut_AppBaseを継承してプログラムを作成 このクラス名から「ベースプログラム」という 名称が変に定着してしまったのかも
継承が便利な時:その2 ポリモフィズム(多態性) 共通の基底クラスから派生したクラスは、基底クラス型のポインタでまとめて管理できる PlugChan *plugs[2]; plugs[0] = new PlugChan(); plugs[1] = new GreatPlugChan(); 基底クラスでうまく共通項を括りだしておくと、派生クラスのバリエーションを効率良く管理できる
オーバーライドと仮想関数 基底型のポインタで管理している限り、基底型で宣言したメンバしか使えない しかし、基底型で宣言した関数を、 派生型で上書きしておくと、基底型の ポインタを通じて呼び出しても派生型で定義した処理を呼び出すことができる この関数上書きをオーバーライドと呼ぶ オーバーライドする関数は、仮想関数としてvirtualを付けておく必要がある
例えば 必殺技を起動する関数specialAttack()を作りたいとする プラグちゃんもグレートプラグちゃんも同じポインタの配列にしまっておきたい でもそれぞれのクラスで別々の処理が ちゃんと呼ばれるようにしたい
こうすればいいのだ class PlugChan { // 前後省略 virtual void specialAttack(void); }; ///////////////////////////////////// void PlugChan::specialAttack(void) { cout << “ろけっとぱ~んち!” << endl; } class GreatPlugChan : public PlugChan{ // 前後省略 void specialAttack(void); // オーバーライドする側はvirtual付けなくていい }; ///////////////////////////////////// void GreatPlugChan::specialAttack(void) { cout << “ウルトラスーパーエクセレントゴージャスろけっとぱ~んち!” << endl; }
俺の名前を呼んでみろ 前のようなスライドにした状態で、次のような呼び出しコードを書いてみる 自動的に処理が切り替わっているはず PlugChan *plugs[2]; plugs[0] = new PlugChan(); plugs[1] = new GreatPlugChan(); plugs[0]->specialAttack(); plugs[1]->specialAttack(); 自動的に処理が切り替わっているはず virtualを付け忘れると切り替わらない
派生クラスの関数から 基底クラスの関数を呼びたい! 基底クラス名::関数名()で呼べる 例えば、entry()関数をオーバーライドしたいが、基底クラスで用意しているものはそちらの処理に任せたい場合 void GreatPlugChan::entry(fkut_SimpleWindow &argWin) { PlugChan::entry(argWin); // この後に追加エントリー処理を書く }
補足:コンソール出力について coutやprintf()の表示は、プロジェクトの設定によって出たり出なかったりします プロジェクトのプロパティから右図の項目を確認して「コンソール」にしておきましょう
継承を使わなかったら どうなるか? 手間はかかるが何とかなる 差分プログラミングの代わりに ポリモフィズムの代わりに JavaとかC#だとそうは行かないが… 差分プログラミングの代わりに 毎回全部のメンバを書いたクラスをコピーして書き換えて使う ポリモフィズムの代わりに 想定される全ての機能、データを盛り込んだクラスを作り、オブジェクトごとにモードを切り替えて使う
継承を使うのは 必要に迫られてからでいい 無理して使うと設計がぐちゃぐちゃになって破綻する が、使えると色々スッキリするのは事実 同じコードをコピペしないで済む それぞれ別々の変数を用意しなくて済む スマートになりますよね 条件分岐がオブジェクト生成時だけで済む これが一番でかい