クラスの相互参照、thisポインタ、クラスの多重継承 —勉強しなおしの記録というか学習ノート-16日目-

Unreal Engin4すごい!(語彙力)ってなったので、少しいじりつつ、むかーしかじった事あるC,C++の学びなおしの学習ノート。この連載記事の詳しい趣旨と注意事項は1日目をご覧ください。

目次

クラスの相互参照

メビウスの輪 ループ

ファイル分けしてクラスを管理する場合、クラス間での参照が「どちらかの一方通行」であれば問題ないのですが、「双方向」の参照となる場合、コンパイル時に問題が起こる可能性があります。

双方のクラスのヘッダファイルに、”#include”を使ってもう一方のクラスのヘッダファイルを参照しようとすると、もう一方のクラスをインクルード、でもインクルードした先のクラスのヘッダファイルでももう一方のクラスをインクルード、 もう一方をインクルード…(無限ループって(ry

つまり2つのファイル間で、インクルードが終わる前に、もう一方をインクルードというサイクルから抜け出せず、インクルードが終わらなくなってしまうわけです。

その回避の仕方を、具体例と共にご紹介したいと思います。

前回の”植物クラス”を作ったプログラムに、”人間クラス”を追加して少し改変したものを作ってみたので、それを例に挙げて説明させていただきます。今回は、”植物クラス”を継承した”夕顔クラス”は使わないです。※今回も例のごとくソースコード長くなってしまいました、ご了承くださいm(_ _;)m

ソースコードファイル一覧

human.h

#pragma once
#include <string>

// 外部にあるクラス名
class Plant;

using namespace std;

/********************************
* 人間クラス(植物を育てるマスター)
*/
class Human
{
private:
	Plant* plant; // 育てる植物
	string name; // 名前
	int water; //保有している水の量
	int exp;	// 経験値
	int level; // レベル
	int max_water; // 保有できる水の上限値
	int max_exp;//現在レベルの上限経験値
public:
	Human(); // コンストラクタ
	virtual ~Human(); // デストラクタ
	int get_water(); //保有している水の量のゲッター
	int get_level(); // 現在のレベルのゲッター
	bool check_plant_pointer();//plantが存命かどうかのチェック
	void give_water(); // 植物に水やり
	void fill_the_water(); // 保有できる最大値まで水を満たす
	void raise_exp(int _water); // 経験値を上げる
	void level_up(); // レベルアップさせる
	void info_status();// ステータスを出力
	void tick_master_plant_status();//マスターと植物のターン毎の始めにステータス通知
	void tick_plant(); // 植物のターンごとの決まった行動
	void throw_away();//植物を捨てる
};

human.cpp

#include "human.h"
#include "plant.h"
#include <iostream>
#include <string>

using namespace std;

/*人間クラスのメンバの初期化・定義***************/
//コンストラクタ
Human::Human() :water(10), exp(0), level(1), max_water(10), max_exp(20) {
	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;
	// 植物生成
	plant = new Plant(this);
}
// デストラクタ
Human::~Human() {
	cout << "おお" << name << "よ、しんでしまうとはなさけない…。" << endl;
	if (plant != NULL) {
		delete plant;
		plant = NULL;
	}
}

//保有している水の量のゲッター
int Human::get_water() {
	return water;
}
// 現在のレベルのゲッター
int Human::get_level() {
	return level;
}

//plantが存命かどうかのチェック
bool Human::check_plant_pointer() {
	bool p = (plant != NULL);
	return p;
}
// 植物に水やり
void Human::give_water() {
	int select;
	while (1) {
		cout << "どのくらいあげますか?" << endl
			<< "水の量:";
		cin >> select;
		if (water > select) {//水が足りれば
			water -= select;
			plant->watered(select);//水を与える
			cout << name << "も与えた水の分だけ経験値獲得します。" << endl;
			raise_exp(select); // 水を与えた分だけexpアップ
			break;
		}
		else {// 水が足りなければ
			cout << "水が足りません。" << endl;
		}
	}
}

// 保有できる最大値まで水を満たす
void Human::fill_the_water() {
	int _water;
	cout << "水を溜めました。" << endl;
	_water = max_water - water;
	water = max_water;
	// 増やした水の量だけexpをアップ
	raise_exp(_water);
}

// 経験値を上げる
void Human::raise_exp(int _water) {
	exp += _water;
	info_status();// 現在のステータスを通知
	if (exp >= max_exp) {//expが上限を超えたら
		//レベルアップ
		level_up();
	}
}

// レベルアップさせる
void Human::level_up() {
	cout << "レベルアップ!" << endl;
	level++;
	max_exp *= 2.5;
	max_water *= 2.5;
	max_exp = (int)max_exp; // キャストして整数に丸める
	max_water = (int)max_water; // キャストして整数に丸める
	info_status(); // 現在のステータス通知
}

// ステータスを出力
void Human::info_status() {
	cout << "あなたの現在のレベル:" << level
		<< "、現在の経験値:" << exp
		<< "、次のレベルアップまで後:" << max_exp - exp
		<< "、現在保有している水の量:" << water
		<< "/" << max_water << endl;
}
//マスターのターン毎の決まった行動
void Human::tick_master_plant_status() {
	info_status();// ステータスを表示
	plant->info_status();
}
// 植物のターン毎の決まった行動
void Human::tick_plant() {
	cout << "植物がターン毎の水の吸収をします。" << endl;
	plant->absorb_water(plant->get_level());//水を吸収
	bool p = (plant->get_water() < 0) ? true : false;
	if (p) {// 水が0を下回っていた場合植物を消去
		delete plant;
		plant = NULL;
	}
}

// 植物を捨てる
void Human::throw_away() {
	int select;
	cout << "植物を捨てます。本当に良いですか?" << endl
		<< "捨てる(Yes:1、No:0):";
	cin >> select;
	if (select) {
		cout << "植物を捨てました。" << endl;
		// 植物を消去
		delete plant;
		plant = NULL;
		cout << "あなた…『覚悟して来てる人』…ですよね" << endl;
	}	
}

plant.h

#pragma once
#include <string>

// 外部にあるクラス名
class Human;

using namespace std;

/******************************************
植物クラス(基底クラス)の宣言
*/
class Plant {
protected:/**植物クラスと派生クラス(ユウガオクラス)のみアクセス可********/
	Human* master; // マスターの情報
	string name;//植物の名前
	int water; // 現在の水の量
	int exp;// 現在の経験値
	int level; // レベル
	int max_exp; // 現在レベルの上限経験値
public:/**どこからでもアクセス可***************************/
	Plant(Human* _master); // コンストラクタ
	virtual ~Plant(); // デストラクタ
	void watered(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(); //現在のステータス(経験値やレベル)を出力
};

plant.cpp

#include "plant.h"
#include "human.h"
#include <iostream>

using namespace std;

/*植物クラスのメンバの初期化・定義***************/
//コンストラクタ
Plant::Plant(Human* _master) :water(5), exp(0), level(1), max_exp(10) {
	master = _master;//マスター登録
	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::watered(int _water) {
	water += _water;
	raise_exp(_water);//経験値を上げる
}

// 水を吸収する
void Plant::absorb_water(int _water) {
	water -= _water;
	if (water >= 0) { 
		cout << "植物が水を吸収しました。" << endl;
		raise_exp(1); //経験値を上げる
	}
	else cout << "植物が水を吸収しようとしましたが、吸収できる水がありません!" << endl;
}

//現在の水の量の値のゲッター
int Plant::get_water() {
	return water;
}

//現在のレベルのゲッター
int Plant::get_level() {
	return level;
}

// 経験値を上げる
void Plant::raise_exp(int _water) {
	exp += _water; 
	info_status(); // 現在のステータス通知
	if (water >= 0 && exp >= max_exp) {//expが上限を超えたら
		//レベルアップ
		level_up();
	}
}

// レベルアップさせる
void Plant::level_up() {
	cout << "レベルアップ!" << endl;
	level++;
	max_exp *= 2.5;
	max_exp = (int)max_exp; // キャストして整数に丸める
	info_status(); // 現在のステータス通知
	cout << "レベルアップしたので、現在レベル分、水分を吸収します。" << endl;
	absorb_water(level);//レベル数に応じて水分を吸収する
	cout << "育てている植物がレベルアップしたので、育てているマスターにもボーナス経験値が入ります。" << endl;
	master->raise_exp(level); // レベルに応じてマスターのexpもアップ
}

//現在のステータス(経験値やレベル)を出力
void Plant::info_status() {
	cout << name << "の現在のレベル:" << level
		<< "、現在の経験値:" << exp
		<< "、次のレベルアップまで後:" << max_exp - exp
		<< "、現在の水の量:" << water << endl;
}

main.cpp

#include "human.h"
#include "plant.h"
#include <iostream>
#include <string>

using namespace std;

// グローバル変数
int master_water = 10;

int main() {
	Human* pM = 0;
	pM = new Human;
	while (1) {
		pM->tick_master_plant_status();
		int select = 0;
		cout << "どうしますか?" << endl
			<< "1:水をあげる。" << endl
			<< "2:放置する。" << endl
			<< "3:水を溜める。" << endl
			<< "4:終了する。" << endl
			<< "選択肢を入力してください:";
		cin >> select;
		switch (select) {
		case 1:
			pM->give_water();
			break;
		case 2:
			cout << "放置しました。" << endl;
			break;
		case 3:
			pM->fill_the_water();
			break;
		default:
			pM->throw_away();
			if (pM->check_plant_pointer() == false) {
				delete pM;//マスターのインスタンスを消去
				return 0;
			}
			break;
		}
		pM->tick_plant();//水を吸収&水の量チェック
		if (pM->check_plant_pointer() == false) {
			delete pM;//マスターのインスタンスを消去
			break;
		}
		cout << "-----------------------" << endl;
	}
	cout << "終了します。" << endl;
	return 0;
}

実行結果

あなたの名前は?
名前:ますた
あなたの名前は ますた ですか?
この名前なら'1'、違う名前なら'0':1
ますた さんには、植物を育てていただきます。
あなたの為の植物が生まれました。名前を付けてあげましょう。
名前:プラント
名前は プラント でよろしいですか?
この名前で決定なら'1'、違う名前にしたいなら'0':1
名前は プラント に決まりました。
可愛がってあげてください
あなたの現在のレベル:1、現在の経験値:0、次のレベルアップまで後:20、現在保有している水の量:10/10
プラントの現在のレベル:1、現在の経験値:0、次のレベルアップまで後:10、現在の水の量:5
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:1
どのくらいあげますか?
水の量:8
プラントの現在のレベル:1、現在の経験値:8、次のレベルアップまで後:2、現在の水の量:13
ますたも与えた水の分だけ経験値獲得します。
あなたの現在のレベル:1、現在の経験値:8、次のレベルアップまで後:12、現在保有している水の量:2/10
植物がターン毎の水の吸収をします。
植物が水を吸収しました。
プラントの現在のレベル:1、現在の経験値:9、次のレベルアップまで後:1、現在の水の量:12
-----------------------
あなたの現在のレベル:1、現在の経験値:8、次のレベルアップまで後:12、現在保有している水の量:2/10
プラントの現在のレベル:1、現在の経験値:9、次のレベルアップまで後:1、現在の水の量:12
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:3
水を溜めました。
あなたの現在のレベル:1、現在の経験値:16、次のレベルアップまで後:4、現在保有している水の量:10/10
植物がターン毎の水の吸収をします。
植物が水を吸収しました。
プラントの現在のレベル:1、現在の経験値:10、次のレベルアップまで後:0、現在の水の量:11
レベルアップ!
プラントの現在のレベル:2、現在の経験値:10、次のレベルアップまで後:15、現在の水の量:11
レベルアップしたので、現在レベル分、水分を吸収します。
植物が水を吸収しました。
プラントの現在のレベル:2、現在の経験値:11、次のレベルアップまで後:14、現在の水の量:9
育てている植物がレベルアップしたので、育てているマスターにもボーナス経験値が入ります。
あなたの現在のレベル:1、現在の経験値:18、次のレベルアップまで後:2、現在保有している水の量:10/10
-----------------------
あなたの現在のレベル:1、現在の経験値:18、次のレベルアップまで後:2、現在保有している水の量:10/10
プラントの現在のレベル:2、現在の経験値:11、次のレベルアップまで後:14、現在の水の量:9
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:終了する。
選択肢を入力してください:4
植物を捨てます。本当に良いですか?
捨てる(Yes:1、No:0):1
植物を捨てました。
おきのどくですが、あなたのプラントは枯れてしまいました。
どうか次の植物は大切に育ててあげてください。
あなた…『覚悟して来てる人』…ですよね
おおますたよ、しんでしまうとはなさけない…。

解説・説明

最初に先ほどの、「無限ループインクルード」を回避する方法ですが、↓のように、最初に”#include”を使わず、外部のクラスを明示しておきます。

  • human.h
#pragma once
#include <string>

// 外部にあるクラス名
class Plant;   // ←ここ

using namespace std;

/********************************
* 人間クラス(植物を育てるマスター)
*/
class Human
{
private:
	Plant* plant; // 育てる植物
	string name; // 名前
 /** 以下略 */

  • plant.h
#pragma once
#include <string>

// 外部にあるクラス名
class Human;   // ←ここ

using namespace std;

/******************************************
植物クラス(基底クラス)の宣言
*/
class Plant {
protected:/**植物クラスと派生クラス(ユウガオクラス)のみアクセス可********/
	Human* master; // マスターの情報
	string name;//植物の名前
/** 以下略 */

ちなみに、 最初に”new”でメモリ動的確保して”植物クラス”のインスタンスの管理を”人間クラス”に任せてしまっています。

/** 前略 */
/*人間クラスのメンバの初期化・定義***************/
//コンストラクタ
Human::Human() :water(10), exp(0), level(1), max_water(10), max_exp(20) {
                /*
                略
                */
	// 植物生成
	plant = new Plant(this);
}
/** 以下略 */

メモリ動的確保したものは、必ず”delete”しないと『メモリリーク』になってしまうのですが、一方で「一度”delete”したものを再度”delete”してはいけない」という決まりもあります。

なので、二重の”delete”をしてしまうのを防ぐために、”delete”した後に、ポインタメンバ変数”plant”に”NULL“を格納して、”delete”済みかの判断に使ってみました。

  • human.cppより抜粋
/** 前略 */
//plantが存命かどうかのチェック
bool Human::check_plant_pointer() {
	bool p = (plant != NULL);
	return p;
}
     /*
     略
     */
// 植物を捨てる
void Human::throw_away() {
	int select;
	cout << "植物を捨てます。本当に良いですか?" << endl
		<< "捨てる(Yes:1、No:0):";
	cin >> select;
	if (select) {
		cout << "植物を捨てました。" << endl;
		// 植物を消去
		delete plant;
		plant = NULL;
		cout << "あなた…『覚悟して来てる人』…ですよね" << endl;
	}	
}

thisポインタ

ちなみになのですが、ここで使っている”this”についてもご紹介します。

  • human.cpp
/** 前略 */
/*人間クラスのメンバの初期化・定義***************/
//コンストラクタ
Human::Human() :water(10), exp(0), level(1), max_water(10), max_exp(20) {
                /*
                略
                */
	// 植物生成
	plant = new Plant(this);
}
/** 以下略 */

↑は、『thisポインタ』と言って、自身のポインタを渡す(そのまんま)というものです。ここでは、「”人間クラス”から生成された”インスタンス”のポインタ」が、「”Plantクラス”のコンストラクタで定義されている引数に」渡されているわけです。

  • plant.cpp
/** 前略 */
/*植物クラスのメンバの初期化・定義***************/
//コンストラクタ
Plant::Plant(Human* _master) :water(5), exp(0), level(1), max_exp(10) {
	master = _master;//マスター登録
/** 以下略 */

クラスの多重継承

多重継承 プログラミング 叫び 植物 マンドレイク

実は、クラスは複数クラスからの継承も可能です。

可能なのですが…、実際は特殊な事例(純粋仮想関数など)を覗いてほとんど実装されません。

ちなみに、『基底クラス』が一つしかない継承を『単一継承』。『基底クラス』が複数ある継承を『多重継承』と言います。

基底 派生 植物 夕顔
多重継承 プログラミング 叫び 植物 マンドレイク

書式は↓のようになります。

class 派生クラス名: public 基底クラス1, public 基底クラス2...{/*以下略*/

(詳しい使い方は後日、『純粋仮想関数』について取り扱う時にでも書かせていただきますね。)

ちなみに、自己満足で無理やり作ってしまった『多重継承』のクラス使ったプログラムがあるのですが、推奨できない使い方なので、番外編に置いておきます。(完全に趣味みたいな感じな上、ソースコード例のごとく長いのですが、「それでも良いよ!」っていう方は覗いてみてやってくださいm(_ _;)m)

あとがき

ご意見・ご感想・ご質問、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄やtwitterアカウントほろほろり(@_horo_horori)へお願いしますm(_ _)m

参考にさせていただいたサイト・ページ一覧

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください