継承、派生クラス、protected、オーバーライド、仮想関数 —勉強しなおしの記録というか学習ノート-15日目-
Unreal Engin4すごい!(語彙力)ってなったので、少しいじりつつ、むかーしかじった事あるC,C++の学びなおしの学習ノート。この連載記事の詳しい趣旨と注意事項は1日目をご覧ください。
※2019年6月11日 加筆修正しました。
目次
継承 –派生クラス
さて、クラスの事を最初に説明した時に、仄めかされていただけの朧気な認識しかされていなかった『継承』をした”クラス”。『派生クラス』について語る時が来たのです!!(何キャラ…?)
『クラス』をいくつか作っていれば、その便利さはご存じだと思いますが、もっと、その有能性を実感できる機能があります。
それが『継承』です。
『オブジェクト指向』の基本の概念でもあるのですが、字面通り、自分の持つ機能を「”継承”させる」ということです。
(もうちょい良い言い回しなかったんか?)
継承元のクラスを『基底クラス』や『基本クラス』、『スーパークラス』、親子関係に見立てて『親クラス』と言ったりします。
また、継承した方のクラスを『派生クラス』、『サブクラス』、『子クラス』と言ったりします。
ということで、私的に呼びやすいので『基底クラス』『派生クラス』と呼ばせていただきます。
『基底クラス』のメンバを受け継いでいるので、『派生クラス』は全てのメンバを使えます。
ですが、
…実は全部のメンバには直接アクセスできなかったりします。
アクセス修飾子(再) –public, private, “protected”
“public“、”private“が、『基底クラス』から『派生クラス』へのメンバの「アクセス制御をしている修飾子」になります。
以前の記事で、『アクセス修飾子』初出の際、 表にまとめて”private”、”public”と共に、”protected“という修飾子もさらっとご紹介したのは覚えておいででしょうか?
該当部分を抜粋して再掲載させていただきます。
アクセス修飾子の種類
修飾子 | 説明 |
public | 外部からのアクセスに制限はなく、全範囲からアクセス可能。 |
private | 同じクラス・同じインスタンス内でのみ、アクセス可能。 |
protected | 同じクラス・同じインスタンス、そして派生クラス・そのインスタンス内でのみアクセス可能。 |
この継承の概念が出てくるまでは、”protected”をつけても意味の無かったので、詳しい説明は省いていたのですが、『派生クラス』を作っていくにあたって、こちらも触れていこうと思います。
派生クラスの基本
派生クラスの宣言の書式は↓のようになります。
class 派生クラス名: public 基底クラス名
{
/*
略
*/
public:
コンストラクタ();
virtual デストラクタ();
/*
略
*/
};
上記のように、派生クラスの後に”:”で区切って「public 基底クラス名」と記述します。そのあとは、メンバを記述する”{}”が来て、そのあとはいつものようにメンバを書くだけです。
ですが、派生クラスを作る時は、基底・派生クラス共に宣言時にデストラクタの前に『virtual修飾子』を記述するのが望ましいそうですので、派生する時には気を付けましょう。
C++言語では、継承を用いる場合、virtualをデストラクタにつけるように推奨されています。
引用元:一週間で身につくC++言語の基本|第6日目:継承
ちなみに、定義では特に変わったことはないです。
上記”virtual”も、宣言時に記述するだけで良いようです。
実際にやってみる
まずは、ソースコードから載せてしまいます。※今回ゲーム作りたい欲が抑えきれず、ソースコードが長くなってしまいました。ご了承くださいm(_ _;)m
ソースコードと実行結果の一覧
- plant.h(基底クラス”Plant”のヘッダファイル)
- plant.cpp(基底クラス”Plant”のソースファイル)
- bottle_gourd.h(派生クラス”Bottle_gourd”のヘッダファイル)
- bottle_gourd.cpp(派生クラス”Bottle_gourd”のソースファイル)
- main.cpp
- 実行結果
plant.h(基底クラス”Plant”のヘッダファイル)
#pragma once
#include <string>
using namespace std;
/******************************************
植物クラス(基底クラス)の宣言
*/
class Plant {
protected:/**植物クラスと派生クラス(ユウガオクラス)のみアクセス可********/
string name;//植物の名前
int water; // 現在の水の量
int exp;// 現在の経験値
int level; // レベル
int max_exp; // 現在レベルの上限経験値
public:/**どこからでもアクセス可***************************/
Plant(); // コンストラクタ
virtual ~Plant(); // デストラクタ
void give_water(int _water); // 水を与える
void absorb_water(int _water); // 水を吸収する
int get_water(); // 現在の水の量の値のゲッター
int get_level(); // 現在のレベルのゲッター
void raise_exp(int _water); // 経験値を上げる
void level_up(); // レベルアップさせる
void info_status(); //現在のステータス(経験値やレベル)を出力
};
plant.cpp(基底クラス”Plant”のソースファイル)
#include "plant.h"
#include <iostream>
using namespace std;
/*植物クラスのメンバの初期化・定義***************/
//コンストラクタ
Plant::Plant() :water(5), exp(0), level(1), max_exp(10) {
int select = 0;
cout << "あなたの為の植物が生まれました。名前を付けてあげましょう。"
<< endl << "名前:";
while (1) {
// 名前を入力してもらう
cin >> name;
cout << "名前は " << name << "でよろしいですか?" << endl
<< "この名前で決定なら'1'、違う名前にしたいなら'0':";
cin >> select;
if (select) break;
else {
cout << "もう一度植物に付けたい名前を入力してください。" << endl
<< "名前:";
}
}
cout << "名前は" << name << "に決まりました。"
<< endl << "可愛がってあげてください" << endl;
}
//デストラクタ
Plant::~Plant() {
cout << "おきのどくですが、あなたの"
<< name << "は枯れてしまいました。" << endl
<< "どうか次の植物は大切に育ててあげてください。"
<< endl;
}
// 水を与える
void Plant::give_water(int _water) {
water += _water;
raise_exp(_water);//経験値を上げる
}
// 水を吸収する
void Plant::absorb_water(int _water) {
cout << "植物が水を吸収しました。" << endl;
water -= _water;
if (water >= 0) {
raise_exp(1);//経験値を上げる
}
}
//現在の水の量の値のゲッター
int Plant::get_water() {
return water;
}
//現在のレベルのゲッター
int Plant::get_level() {
return level;
}
// 経験値を上げる
void Plant::raise_exp(int _water) {
exp += _water;
info_status();// 現在のステータスを通知
if (exp >= max_exp && water >= 0) {//expが上限を超えたら
//レベルアップ
level_up();
}
}
// レベルアップさせる
void Plant::level_up() {
cout << "レベルアップ!" << endl;
level++;
max_exp *= 2.5;
max_exp = (int)max_exp; // キャストして整数に丸める
info_status(); // 現在のステータス通知
absorb_water(level);//レベル数に応じて水分を吸収する
}
//現在のステータス(経験値やレベル)を出力
void Plant::info_status() {
cout << name << "の現在のレベル:" << level
<< "、現在の経験値:" << exp
<< "、次のレベルアップまで後:" << max_exp - exp
<< "、現在の水の量:" << water << endl;
}
bottle_gourd.h(派生クラス”Bottle_gourd”のヘッダファイル)
#pragma once
#include "plant.h"
/****************************
ユウガオクラス(植物クラスの派生クラス)
*/
class Bottle_gourd :public Plant
{
private:
int bloom_count; // 咲くまでのカウント
int bloom_time; // 咲くタイミング
public:
Bottle_gourd(); // コンストラクタ
virtual ~Bottle_gourd(); // デストラクタ
int get_bloom_count(); // 咲くまでのカウントの値のゲッター
void bloom_countup();// 咲くまでのカウントを進める
void reset_bloom_count(); // 咲くまでのカウントリセット
void info_status(); // 咲くまでのカウントを通知
};
bottle_gourd.cpp(派生クラス”Bottle_gourd”のソースファイル)
#include "bottle_gourd.h"
#include <iostream>
using namespace std;
/*ユウガオクラスのメンバの初期化・定義***************/
//コンストラクタ
Bottle_gourd::Bottle_gourd() :bloom_count(0), bloom_time(10) {
cout << "この植物はユウガオです。" << endl;
}
//デストラクタ
Bottle_gourd::~Bottle_gourd() {
cout << "ユウガオは萎れてしまった。" << endl;
}
// 咲くまでのカウントの値のゲッター
int Bottle_gourd::get_bloom_count() {
return bloom_count;
}
// 咲くまでのカウントを進める
void Bottle_gourd::bloom_countup() {
//カウントを進める前から上限値以上になっていたらリセット
if (bloom_count >= bloom_time) reset_bloom_count();
// 現在レベル分咲くカウントを進める
bloom_count += get_level();
//カウントが上限値を上回っていたら超えた分を切り捨て
bloom_count = (bloom_count > bloom_time) ? bloom_time : bloom_count;
}
// 咲くまでのカウントリセット
void Bottle_gourd::reset_bloom_count() {
bloom_count = 0;
}
//現在のステータス(経験値やレベル)を出力
void Bottle_gourd::info_status() {
if (bloom_count >= bloom_time) cout << "満開です!" << endl;
else cout << name << "が咲くまで後:" << bloom_time - bloom_count << endl;
}
main.cpp
#include "plant.h"
#include "bottle_gourd.h"
#include <iostream>
#include <string>
using namespace std;
// グローバル変数
int master_water = 10;
// プロトタイプ宣言
void master_give_water(Bottle_gourd* p); // マスターの水やり
void info_master_states();//マスターの経験値…今は水の量保有量しかありません。
int main() {
Bottle_gourd* pB = 0; // ユウガオクラス用のポインタを初期化
pB = new Bottle_gourd; // メモリ動的確保したインスタンスを生成
while (1) {
int select = 0;
cout << "どうしますか?" << endl
<< "1:水をあげる。" << endl
<< "2:放置する。" << endl
<< "3:水を溜める。" << endl
<< "4:終了する。" << endl
<< "選択肢を入力してください:";
cin >> select;
switch (select) {
case 1:
master_give_water(pB);
break;
case 2:
cout << "放置しました。" << endl;
break;
case 3:
cout << "水を溜めました。" << endl;
master_water = 10;
break;
default:
cout << "終了します。" << endl;
delete pB;//植物クラスのインスタンスを消去
return 0;
}
info_master_states();//マスターの水の保有量を通知
pB->absorb_water(pB->get_level());//水を吸収
pB->bloom_countup(); // 咲くまでのカウンタを進める
pB->info_status(); // 咲き具合を通知
//現在の水の量を確認
int w = pB->get_water();
if (w < 0) {//水が0を下回ったら
delete pB;//植物クラスのインスタンスを消去
break;
}
cout << "-----------------------" << endl;
}
cout << "終了します。" << endl;
return 0;
}
/****マスターの行動用関数*************************************/
// マスターの水やり関数
void master_give_water(Bottle_gourd* p) {
int select;
while (1) {
cout << "どのくらいあげますか?" << endl
<< "水の量:";
cin >> select;
if (master_water > select) {//水が足りれば
master_water -= select;
p->give_water(select);//水を与える
break;
}
else {// 水が足りなければ
cout << "水が足りません。" << endl;
}
}
}
// マスターのステータス(水の保有量)通知
void info_master_states() {
cout << "現在のあなたの保有している水の量:" << master_water << endl;
}
実行結果
あなたの為の植物が生まれました。名前を付けてあげましょう。
名前:プラント
名前は プラントでよろしいですか?
この名前で決定なら'1'、違う名前にしたいなら'0':1
名前はプラントに決まりました。
可愛がってあげてください
この植物はユウガオです。
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:1
どのくらいあげますか?
水の量:5
プラントの現在のレベル:1、現在の経験値:5、次のレベルアップまで後:5、現在の水の量:10
現在のあなたの保有している水の量:5
植物が水を吸収しました。
プラントの現在のレベル:1、現在の経験値:6、次のレベルアップまで後:4、現在の水の量:9
プラントが咲くまで後:9
-----------------------
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:4
終了します。
ユウガオは萎れてしまった。
おきのどくですが、あなたのプラントは枯れてしまいました。
どうか次の植物は大切に育ててあげてください。
解説・説明
派生クラスからインスタンスが生成された時には、「基底クラスのコンストラクタ」→「派生クラスのコンストラクタ」の順に呼び出されます。
あなたの為の植物が生まれました。名前を付けてあげましょう。
名前:プラント
名前は プラントでよろしいですか?
この名前で決定なら'1'、違う名前にしたいなら'0':1
名前はプラントに決まりました。
可愛がってあげてください //この行までは"基底クラス"のコンストラクタ
この植物はユウガオです。 // この行は"派生クラス"のコンストラクタ
/* 以下略 */
ですが逆に、派生クラスから生成されたインスタンスが消去される時は、「派生クラスのデストラクタ」→「基底クラスのデストラクタ」という順で呼び出されています。
/* 前略 */
ユウガオは萎れてしまった。 // この行は"派生クラス"のデストラクタ
おきのどくですが、あなたのプラントは枯れてしまいました。 // この行から"基底クラス"のデストラクタ
どうか次の植物は大切に育ててあげてください。
今回の作った『植物クラス(基底クラス)』の宣言の時に、メンバ変数は全て“protected”で指定しているのにお気づきでしょうか?
このようにしておけば、『ユウガオクラス(植物クラスの派生クラス)』からも 『植物クラス(基底クラス)』で定義されているメンバ変数に自由にアクセスできます。
オーバーライド
以前扱ったオーバーロードとは、別物です。
上の項のソースコードの、 『ユウガオクラス(植物クラスの派生クラス)』、 『植物クラス(基底クラス)』 ともに、実は、”void info_status()“という、「“型”も”引数”も同じメンバ関数」が存在します。
こういうものを『オーバーライド』と言います。
ですが、実行結果を見てみると、
/* 前略 */
プラントの現在のレベル:1、現在の経験値:5、次のレベルアップまで後:5、現在の水の量:10 // "基底クラス"のinfo_status()
現在のあなたの保有している水の量:5
植物が水を吸収しました。
プラントの現在のレベル:1、現在の経験値:6、次のレベルアップまで後:4、現在の水の量:9 // "基底クラス"のinfo_status()
プラントが咲くまで後:9 // "派生クラス"のinfo_status()
/* 以下略 */
それぞれちゃんと存在しています。
というのも、基底・派生クラスでそれぞれ「同じ”型”と”引数”のメンバ関数」が存在する場合、
- 呼び出されている場所が同じクラスの場合は、その同じクラスのメンバ関数が呼ばれる。
- 外からクラス外から直接呼ばれた場合、派生クラス側が呼ばれます。
また、例えば派生クラス側から基底クラスの方のオーバーライドしている関数を呼び出したい場合は、「スコープ解決演算子(“::”)」を使って、「Plant::info_status(); 」のように呼び出せば解決できます。
コラム:ポリモーフィズム
『ポリモーフィズム』とは、オブジェクト指向における大事な概念の1つです。「多様性」という意味の言葉で、『オーバーロード』、『オーバーライド』は、この『ポリモーフィズム』の一例です。
メンバの名前が統一されることにより、名前を覚える必要がなくなり、タイプミスも減らせるというメリットがあります。
オブジェクト指向では、原則的に同じようなの機能には共通する名前を付けることが好まれるので、ほとんど同じような処理でも多少違う処理のメンバ関数(関数)に共通の単語などを入れることにより、プログラムに統一感を持たせられるというものです。
仮想関数 –virtual
先ほども、派生クラスを作る時に、デストラクタ前でつけて欲しいと言いました“virtual”修飾子ですが、実はこれ、こういう使い方できるんですよ。というので書いておきます。
前提としてメンバ関数が「基底・派生クラス」で『オーバーライド』しているとします。
その時に、「基底クラスの方で使っているものを、派生クラス使ってるときは上書きして使いたい!」となる場合も考えられます。
その場合、基底クラスの方の『オーバーライド』している関数の、ヘッダでの宣言時に関数名の前に”virtual“を付けます。
そうすると、その基底クラスの『オーバーライド』している関数は、派生クラスの方で、宣言・定義された方の関数で上書きされます。つまり、使われる先々で ”info_status(); ”として出てくるのは、派生クラスの方の” info_status(); “になります。
ということで、ちょっと先ほどのソースコードを使いまわします。該当箇所だけ抜粋↓
- plant.h(基底クラス”Plant”のヘッダファイル)
#pragma once
#include <string>
using namespace std;
/******************************************
植物クラス(基底クラス)の宣言
*/
class Plant {
protected:/**植物クラスと派生クラス(ユウガオクラス)のみアクセス可********/
string name;//植物の名前
int water; // 現在の水の量
int exp;// 現在の経験値
int level; // レベル
int max_exp; // 現在レベルの上限経験値
public:/**どこからでもアクセス可***************************/
Plant(); // コンストラクタ
virtual ~Plant(); // デストラクタ
void give_water(int _water); // 水を与える
void absorb_water(int _water); // 水を吸収する
int get_water(); // 現在の水の量の値のゲッター
int get_level(); // 現在のレベルのゲッター
void raise_exp(int _water); // 経験値を上げる
void level_up(); // レベルアップさせる
virtual void info_status(); //現在のステータス(経験値やレベル)を出力 //←"virtual"を付けて仮想関数化/////////////////////////
};
- 実行結果
あなたの為の植物が生まれました。名前を付けてあげましょう。
名前:プラント
名前は プラントでよろしいですか?
この名前で決定なら'1'、違う名前にしたいなら'0':1
名前はプラントに決まりました。
可愛がってあげてください
この植物はユウガオです。
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:1
どのくらいあげますか?
水の量:5
プラントが咲くまで後:10
現在のあなたの保有している水の量:5
植物が水を吸収しました。
プラントが咲くまで後:10
プラントが咲くまで後:9
-----------------------
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:4
終了します。
ユウガオは萎れてしまった。
おきのどくですが、あなたのプラントは枯れてしまいました。
どうか次の植物は大切に育ててあげてください。
実行結果を同じ場所だけ比べて見てみると、一目瞭然ですね。
“virtual”無し
選択肢を入力してください:1
どのくらいあげますか?
水の量:5
プラントの現在のレベル:1、現在の経験値:5、次のレベルアップまで後:5、現在の水の量:10
現在のあなたの保有している水の量:5
植物が水を吸収しました。
プラントの現在のレベル:1、現在の経験値:6、次のレベルアップまで後:4、現在の水の量:9
プラントが咲くまで後:9
“virtual”有り
選択肢を入力してください:1
どのくらいあげますか?
水の量:5
プラントが咲くまで後:10
現在のあなたの保有している水の量:5
植物が水を吸収しました。
プラントが咲くまで後:10
プラントが咲くまで後:9
先ほどは、両方のクラスの”info_status();”が混在していましたが、今回のものは、出力される全てが『派生クラス』の方に置き換わっています。
分かりやすくするために、そのまま基底クラスの宣言時に関数名の前に”virtual”を付けるという変更をしましたが、これでは、”現在レベル”や”経験値”が分かりません。なので、派生クラスの “info_status()”定義の 方も少し書き換えてみます。
- bottle_gourd.cpp(派生クラス”Bottle_gourd”のソースファイル)
/* 前略 */
//現在のステータス(経験値やレベル)を出力
void Bottle_gourd::info_status() {
cout << name << "現在" << bloom_count << "咲き。"<< "現在のレベル:" << level
<< "、現在の経験値:" << exp
<< "、次のレベルアップまで後:" << max_exp - exp
<< "、現在の水の量:" << water << endl;
if (bloom_count >= bloom_time) cout << "満開です!" << endl;
else cout << name << "が咲くまで後:" << bloom_time - bloom_count << endl;
}
- 実行結果(一部違いが確認できる場所だけ抜粋)
選択肢を入力してください:1
どのくらいあげますか?
水の量:5
プラント現在0咲き。現在のレベル:1、現在の経験値:5、次のレベルアップまで後:5、現在の水の量:10
プラントが咲くまで後:10
現在のあなたの保有している水の量:5
植物が水を吸収しました。
プラント現在0咲き。現在のレベル:1、現在の経験値:6、次のレベルアップまで後:4、現在の水の量:9
プラントが咲くまで後:10
プラント現在1咲き。現在のレベル:1、現在の経験値:6、次のレベルアップまで後:4、現在の水の量:9
プラントが咲くまで後:9
これだと、使い方の幅が広く見えてくるでしょう!?(興奮して語気が荒い
あとがき
ご意見・ご感想・ご質問、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄やtwitterアカウントほろほろり(@_horo_horori)へお願いしますm(_ _)m
参考にさせていただいたサイト・ページ一覧
- 一週間で身につくC言語の基本|第6日目:継承
- 継承
- 一週間で身につくC++言語の基本|第7日目:ポリモーフィズム
- ロベールのC++教室 – 第11章 子孫 –
- 仮想関数
- ロベールのC++教室 – 第14章 仮想関数 –