ジャンル雑多なゲーム・ゲーム制作関連の色々な情報を取り扱っているブログ。最近はBlenderについてが中心。
[C, C++学習]C, C++言語再学習ノート-18日目- –純粋仮想関数、抽象クラス、ポインタ渡しでの注意点

[C, C++学習]C, C++言語再学習ノート-18日目- –純粋仮想関数、抽象クラス、ポインタ渡しでの注意点

純粋仮想関数、抽象クラス、ポインタ渡しでやらかした話 —勉強しなおしの記録というか学習ノート-18日目-

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

※2019年6月15日 ソースコード表示・非表示切替ボタンがどのボタンも一番上用の切り替えボタンになっていたミスを修正

目次

純粋仮想関数 –virtualと”0″で作る、基底クラスの「存在しないメンバ関数」

まずは項目名にもある『virtual修飾子』ですが、これは(主に基底クラスなどの派生元)クラスのメンバ関数を『仮想関数』化させる時に使うのでした。

では、項目にある『純粋仮想関数』とは?とお思いだと思います。

最初にざっくりと説明すれば『純粋仮想関数』は、「実際は存在するけれど、存在しないのと同じ扱いをされる関数」のことです。

class クラス名{ // 基底クラスなど、継承されること前提のクラス
private:
    /* 略 */
public:
    /* 略 */
    void 関数名() = 0;
}

↑の、”=0″で、「名前はあるけど、実装されていない」メンバ関数が、『純粋仮想関数』です。

抽象クラス

基底 派生 植物 夕顔

こういった『純粋仮想関数』を持つクラスは『抽象クラス』と言って、このクラスの直接のインスタンスは生成できなくなります。

基本的に、メンバ関数は実装しなければいけないものです。

ですがこの『抽象クラス』は、 『基底クラス』となることを前提として定義し、『純粋仮想関数』の実装を継承先の、”実体”(インスタンス)を持つ『派生クラス』にさせるための、その名の通り”抽象的概念”のような存在です。

「実装を継承先に任せる」ということで、『純粋仮想関数』を『派生クラス』の方で実装しなければその『派生クラス』も『抽象クラス』化してしまうため、インスタンス作ると”エラー”します。つまり、実装し忘れ防止にもなりますね。

『抽象クラス化』してはいなかったんですが、以前作った「植物クラス」と「夕顔クラス」を例に取ってみると、

植物」という”言葉”と”概念”は存在しますが、
「植物」というのは”抽象的”な表現であり、
固有の”実体”を伴っていないため「存在しない」ものとも言える訳です。

一方で、「夕顔」の方は、
「植物」の一種として”具体的”に個を指す言葉であり、
「夕顔」という固有の”実体”を伴うものが実際に「存在する」ものです。

という感じで考えると分かりやすいかと思います。

ということで、『純粋仮想関数』を使ってプログラム作ってみました。(※また長くなってしまいましたご了承ください)

純粋仮想関数を使ってみた

植物 継承 クラス プログラミング  抽象クラス 純粋仮想関数 派生クラス 基底クラス

plant.h(抽象クラスのヘッダファイル)

#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 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(); //現在のステータス(経験値やレベル)を出力

	virtual bool tick_plant() = 0; // 毎ターン最後に水を吸収&水の量チェック(純粋仮想関数)
	virtual bool throw_away() = 0; // 捨てる

	/*夕顔用の純粋仮想関数********************/
	virtual int get_bloom_count() = 0; // 咲くまでのカウントの値のゲッター
	virtual void bloom_countup() = 0;// 咲くまでのカウントを進める
	virtual void reset_bloom_count() = 0; // 咲くまでのカウントリセット
	/* マンドレイク用の純粋仮想関数 ****************************/
	virtual void set_serif(string _serif) = 0;// セリフのセッター
	virtual string get_serif() = 0; //セリフのゲッター
	virtual void set_voice_size(int _size) = 0; // 声の大きさのセッター
	virtual int get_voice_size() = 0;//声の大きさのゲッター
	virtual void limit_countdown() = 0;//カウントダウン
	virtual int get_time_limit() = 0;// タイムリミット値のゲッター
	virtual int get_max_time_limit() = 0;// タイムリミット上限値のゲッター
	virtual void reset_time_limit() = 0;//タイムリミットをリセット
	virtual void show_time_limit() = 0;// 叫びを聞いていられるタイムリミット通告
};

plant.cpp(抽象クラスのソースファイル)

#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::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) {	level_up();	}//expが上限を超えたらレベルアップ
}

// レベルアップさせる
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;
}

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

bottle_gourd.h(派生クラスその1のヘッダファイル)

#pragma once
#include "plant.h"
#include <iostream>
#include <string>

/****************************
ユウガオクラス(植物クラスの派生クラス)
*/
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();  // ステータス通知
	bool tick_plant(); // 毎ターン最後に水を吸収&水の量チェック
	bool throw_away(); //捨てる

	/* マンドレイク用の純粋仮想関数 ****************************/
	void set_serif(string _serif) {}// セリフのセッター
	string get_serif() { return "";} //セリフのゲッター
	void set_voice_size(int _size) {} // 声の大きさのセッター
	int get_voice_size() { return 0; }//声の大きさのゲッター
	void limit_countdown() {}//カウントダウン
	int get_time_limit() { return 0; }// タイムリミット値のゲッター
	int get_max_time_limit() { return 0; }// タイムリミット上限値のゲッター
	void reset_time_limit() {}//タイムリミットをリセット
	void show_time_limit() {}// 叫びを聞いていられるタイムリミット通告
};

bottle_gourd.cpp(派生クラスその1のソースファイル)

#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() {
	cout << name << "現在" << bloom_count << "咲き。" << "現在のレベル:" << level
		<< "、現在の経験値:" << exp
		<< "、次のレベルアップまで後:" << max_exp - exp
		<< "、現在の水の量:" << water;
	if (bloom_count >= bloom_time)	 cout << " 満開です!" << endl;
	else cout << " 咲くまで後:" << bloom_time - bloom_count << endl;
}
// 毎ターン最後に水を吸収&水の量チェック
bool Bottle_gourd::tick_plant() {
	cout << "植物がターン毎の水の吸収をします。" << endl;
	absorb_water(get_level());//水を吸収
	if (get_water() < 0) return false; // フラグ用にfalseを返す
	else return true;
}

bool Bottle_gourd::throw_away() {//捨てる
	int select;
	cout << "本当に良いですか?" << endl << "捨てる(Yes:1、No:0):";
	cin >> select;
	if (select) { // 捨てる
		cout << "植物を捨てました。" << endl;
		return false;
	}
	else return true;
}

mandrake.h(派生クラスその2のヘッダファイル)

#pragma once
#include "plant.h"

/***********************
* マンドレイククラス(派生クラス, 継承元:植物クラス)
*/
class Mandrake:public Plant{
private:
	string serif;//セリフ(叫ぶ内容)
	int voice_size; // 声の大きさ
	int time_limit; // 叫びを聞いていられるタイムリミット
	int max_time_limit; //タイムリミット上限値
public:
	Mandrake(); // コンストラクタ
	virtual ~Mandrake(); // デストラクタ
	void set_serif(string _serif);// セリフのセッター
	string get_serif(); //セリフのゲッター
	void set_voice_size(int _size); // 声の大きさのセッター
	int get_voice_size();//声の大きさのゲッター
	void limit_countdown();//カウントダウン
	int get_time_limit();// タイムリミット値のゲッター
	int get_max_time_limit();// タイムリミット上限値のゲッター
	void reset_time_limit();//タイムリミットをリセット
	void show_time_limit();// 叫びを聞いていられるタイムリミット通告
	bool tick_plant(); // 毎ターン最後に水を吸収&水の量チェック
	bool throw_away(); // 捨てる

	/**夕顔用*************************/
	int get_bloom_count() {return 0;}// 咲くまでのカウントの値のゲッター
	void bloom_countup() {}// 咲くまでのカウントを進める
	void reset_bloom_count() {} // 咲くまでのカウントリセット
};

mandrake.cpp(派生クラスその2のヘッダファイル)

#include "mandrake.h"
#include <iostream>
#include <string>

using namespace std;

/* マンドレイククラスのメンバ初期化・定義 **************************************/
// コンストラクタ
Mandrake::Mandrake() :serif(string("ぴぎゃーーーーーー")), voice_size(5), time_limit(5), max_time_limit(5) {
	cout << "この植物はマンドレイクです。※お取り扱いには十分ご注意ください。" << endl;
}

// デストラクタ
Mandrake::~Mandrake() {	cout << name <<"は萎れてしまいました。" << endl;}

// セリフのセッター
void Mandrake::set_serif(string _serif) {serif = _serif;}

// セリフのゲッター
string Mandrake::get_serif() {return serif;}

// 声の大きさのセッター
void Mandrake::set_voice_size(int _size) {voice_size = _size;}
// 声の大きさのゲッター
int Mandrake::get_voice_size() {return voice_size;}

//カウントダウン
void Mandrake::limit_countdown() {time_limit--;}

// タイムリミット値のゲッター
int Mandrake::get_time_limit() {return time_limit;}
// タイムリミット上限値のゲッター
int Mandrake::get_max_time_limit() {return max_time_limit;}

//タイムリミットをリセット
void Mandrake::reset_time_limit() {	time_limit = max_time_limit;}

// 叫びを聞いていられるタイムリミット通告
void Mandrake::show_time_limit() {
	string a; // 叫びの「!」セット用
	for (int i = 0; i < voice_size; i++) {
		a.append("!");
	}
	cout << "危険!マンドレイクが泣き叫んでいます!" << endl;
	cout << serif + a << endl; // 植物クラスから引っ張ってきたメンバ関数
	cout << "今から後" << time_limit << "ターンであなたは覚めない眠りに就くでしょう…。" << endl;
}
// 毎ターン最後に水を吸収&水の量チェック
bool Mandrake::tick_plant() {
	cout << "植物がターン毎の水の吸収をします。" << endl;
	absorb_water(get_level());//水を吸収

	if (get_water() < 0) {
		if (get_time_limit() <= 0) return false; // deleteフラグ用にfalseを返す
		else {
			// タイムリミットがまだセーフ
			show_time_limit();// 叫ぶ
			limit_countdown();// リミットを1つずつ減らす
		}
	}
	else if (get_max_time_limit() != get_time_limit()) {
		// 水の量が0より多く、タイムリミットが上限値ではない場合、タイムリミットリセット
		reset_time_limit();
	}
	return true;// if文を抜けたらtrueを返す
}
// 捨てる
bool Mandrake::throw_away() {
	int select;
	cout << "本当に良いですか?" << endl << "捨てる(Yes:1、No:0):";
	cin >> select;
	if (select) { // 捨てる
		cout << "植物を捨てました。" << endl << "あなた…『覚悟して来てる人』…ですよね" << endl;
		return false;
	}
	else true;
}

human.h

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

using namespace std;

/********************************
* 人間クラス(植物を育てるマスター)
*/
class Human
{
private:
	string name; // 名前
	int water; //保有している水の量
	int exp;	// 経験値
	int level; // レベル
	int max_water; // 保有できる水の上限値
	int max_exp;//現在レベルの上限経験値
public:
	Human(); // コンストラクタ
	virtual ~Human(); // デストラクタ
	int get_water(); //保有している水の量のゲッター
	int get_level(); // 現在のレベルのゲッター
	int give_water(); // 植物に水やり
	void full_the_water(); // 保有できる最大値まで水を満たす
	void raise_exp(int _water); // 経験値を上げる
	void level_up(); // レベルアップさせる
	void info_status();// ステータスを出力
};

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;
}
// デストラクタ
Human::~Human() {cout << "おお" << name << "よ、しんでしまうとはなさけない…。" << endl;}

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

// 植物に水やり
int Human::give_water() {
	int select;
	while (1) {
		cout << "どのくらいあげますか?" << endl
			<< "水の量:";
		cin >> select;
		if (water > select) {//水が足りれば
			water -= select;
			return select; // 与える水の量を返す
		}
		else {// 水が足りなければ
			cout << "水が足りません。" << endl;
			return 0;
		}
	}
}

// 保有できる最大値まで水を満たす
void Human::full_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;
}

main.cpp

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

using namespace std;

//列挙型
enum PLANTS_NUMBER { YUGAO, MANDRAKE };
//解放判断用フラグ
bool FLAG_YUGAO = true, FLAG_MANDRAKE = true;
// プロトタイプ宣言
int select_plant();

int main() {
	Plant* p1, * p2;
	Human* pM = new Human; // 人間クラスのインスタンス生成
	p1 = new Bottle_gourd; // 夕顔クラスのインスタンス生成
	p2 = new Mandrake; // マンドレイククラのインスタンス生成
	
	int select = 0, water = 0;
	while (1){
		pM->info_status();// 人間ステータス表示
		if (FLAG_YUGAO) p1->info_status(); 
		if (FLAG_MANDRAKE) p2->info_status();// マンドレイクステータス表示
		
		cout << "どうしますか?" << endl
			<< "1:水をあげる。" << endl
			<< "2:放置する。" << endl
			<< "3:水を溜める。" << endl
			<< "4:捨てる。" << endl
			<< "選択肢を入力してください:";
		cin >> select;
		switch (select) {
		case 1://水やり
			if (FLAG_YUGAO) select = select_plant(); // どちらの植物か選択
			else select = MANDRAKE;

			water = pM->give_water();// 水が足りるかチェック and 足りた場合は与える水の量を返す
			if (water > 0) {
				if (select == YUGAO)p1->watered(water);//水を与える
				if (select == MANDRAKE)p2->watered(water);//水を与える

				cout << "あなたも与えた水の分だけ経験値獲得します。" << endl;
				pM->raise_exp(select); // 水を与えた分だけexpアップ
			}
			break;
		case 2:// 放置
			cout << "放置しました。" << endl;
			break;
		case 3://水を確保し直し
			pM->full_the_water();
			break;
		default://捨てる
			if (FLAG_YUGAO) select = select_plant();// どちらの植物か選択
			else select = MANDRAKE;

			if (select == YUGAO)FLAG_YUGAO = p1->throw_away();
			if (select == MANDRAKE)FLAG_MANDRAKE = p2->throw_away();
			break;
		}
		//水を吸収&水の量チェック
		if(FLAG_YUGAO) FLAG_YUGAO = p1->tick_plant();//夕顔
		//if(!FLAG_YUGAO)delete p1;//もし夕顔が枯れたらインスタンス消去
		if(FLAG_MANDRAKE) FLAG_MANDRAKE = p2->tick_plant();//マンドレイク

		if (!FLAG_MANDRAKE) {//マンドレイクフラグが立っていたら
			delete pM;//マスターのインスタンスを消去
			delete p2;//マンドレイクのインスタンス消去
			delete p1;//もし夕顔がまだ存在していたら、消去
			return 0;
		}
		cout << "-----------------------" << endl;
	}
}

//植物選択関数
int select_plant() {
	int select;
	while (1) {
		cout << "どっち?" << endl
			<< "ユウガオなら'0', マンドレイクなら'1':";
		cin >> select;
		if (select == 0 || select == 1) break;
		else cout << "'0'か'1'で指定してください。" << endl;
	}
	return select;
}

実行結果

あなたの名前は?
名前:マスター
あなたの名前は マスター ですか?
この名前なら'1'、違う名前なら'0':1
マスター さんには、植物を育てていただきます。
あなたの為の植物が生まれました。名前を付けてあげましょう。
名前:ユウガオ
名前は ユウガオ でよろしいですか?
この名前で決定なら'1'、違う名前にしたいなら'0':1
名前は ユウガオ に決まりました。
可愛がってあげてください
この植物はユウガオです。
あなたの為の植物が生まれました。名前を付けてあげましょう。
名前:まんどれいく
名前は まんどれいく でよろしいですか?
この名前で決定なら'1'、違う名前にしたいなら'0':1
名前は まんどれいく に決まりました。
可愛がってあげてください
この植物はマンドレイクです。※お取り扱いには十分ご注意ください。
あなたの現在のレベル:1、現在の経験値:0、次のレベルアップまで後:20、現在保有している水の量:10/10
ユウガオ現在0咲き。現在のレベル:1、現在の経験値:0、次のレベルアップまで後:10、現在の水の量:5 咲くまで後:10
まんどれいくの現在のレベル:1、現在の経験値:0、次のレベルアップまで後:10、現在の水の量:5
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:捨てる。
選択肢を入力してください:1
どっち?
ユウガオなら'0', マンドレイクなら'1':0
どのくらいあげますか?
水の量:9
ユウガオ現在0咲き。現在のレベル:1、現在の経験値:9、次のレベルアップまで後:1、現在の水の量:14 咲くまで後:10
あなたも与えた水の分だけ経験値獲得します。
あなたの現在のレベル:1、現在の経験値:0、次のレベルアップまで後:20、現在保有している水の量:1/10
植物がターン毎の水の吸収をします。
植物が水を吸収しました。
ユウガオ現在0咲き。現在のレベル:1、現在の経験値:10、次のレベルアップまで後:0、現在の水の量:13 咲くまで後:10
レベルアップ!
ユウガオ現在0咲き。現在のレベル:2、現在の経験値:10、次のレベルアップまで後:15、現在の水の量:13 咲くまで後:10
レベルアップしたので、現在レベル分、水分を吸収します。
植物が水を吸収しました。
     /************** 中略 ******************************************************************************************/
-----------------------
あなたの現在のレベル:2、現在の経験値:23、次のレベルアップまで後:27、現在保有している水の量:10/25
ユウガオ現在0咲き。現在のレベル:3、現在の経験値:34、次のレベルアップまで後:28、現在の水の量:4 咲くまで後:10
まんどれいくの現在のレベル:1、現在の経験値:5、次のレベルアップまで後:5、現在の水の量:-4
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:捨てる。
選択肢を入力してください:1
どっち?
ユウガオなら'0', マンドレイクなら'1':0
どのくらいあげますか?
水の量:8
ユウガオ現在0咲き。現在のレベル:3、現在の経験値:42、次のレベルアップまで後:20、現在の水の量:12 咲くまで後:10
あなたも与えた水の分だけ経験値獲得します。
あなたの現在のレベル:2、現在の経験値:23、次のレベルアップまで後:27、現在保有している水の量:2/25
植物がターン毎の水の吸収をします。
植物が水を吸収しました。
ユウガオ現在0咲き。現在のレベル:3、現在の経験値:43、次のレベルアップまで後:19、現在の水の量:9 咲くまで後:10
植物がターン毎の水の吸収をします。
植物が水を吸収しようとしましたが、吸収できる水がありません!
危険!マンドレイクが泣き叫んでいます!
ぴぎゃーーーーーー!!!!!
今から後1ターンであなたは覚めない眠りに就くでしょう…。
-----------------------
あなたの現在のレベル:2、現在の経験値:23、次のレベルアップまで後:27、現在保有している水の量:2/25
ユウガオ現在0咲き。現在のレベル:3、現在の経験値:43、次のレベルアップまで後:19、現在の水の量:9 咲くまで後:10
まんどれいくの現在のレベル:1、現在の経験値:5、次のレベルアップまで後:5、現在の水の量:-5
どうしますか?
1:水をあげる。
2:放置する。
3:水を溜める。
4:捨てる。
選択肢を入力してください:1
どっち?
ユウガオなら'0', マンドレイクなら'1':0
どのくらいあげますか?
水の量:1
ユウガオ現在0咲き。現在のレベル:3、現在の経験値:44、次のレベルアップまで後:18、現在の水の量:10 咲くまで後:10
あなたも与えた水の分だけ経験値獲得します。
あなたの現在のレベル:2、現在の経験値:23、次のレベルアップまで後:27、現在保有している水の量:1/25
植物がターン毎の水の吸収をします。
植物が水を吸収しました。
ユウガオ現在0咲き。現在のレベル:3、現在の経験値:45、次のレベルアップまで後:17、現在の水の量:7 咲くまで後:10
植物がターン毎の水の吸収をします。
植物が水を吸収しようとしましたが、吸収できる水がありません!
おおマスターよ、しんでしまうとはなさけない…。
まんどれいくは萎れてしまいました。
おきのどくですが、あなたのまんどれいくは枯れてしまいました。
どうか次の植物は大切に育ててあげてください。
ユウガオは萎れてしまった。
おきのどくですが、あなたのユウガオは枯れてしまいました。
どうか次の植物は大切に育ててあげてください。

説明・解説

今回は、「植物クラス」から派生クラス二つのインスタンスのポインタを作ったので、更にソースコードが冗長なことになってしまいました(~_~;)

main.cppより抜粋

    /*略*/
    Plant* p1, * p2;
	Human* pM = new Human; // 人間クラスのインスタンス生成
	p1 = new Bottle_gourd; // 夕顔クラスのインスタンス生成
	p2 = new Mandrake; // マンドレイククラのインスタンス生成
    /*略*/

以下、「抽象クラス(基底クラス)」、「派生クラスその1」、「派生クラスその2」のヘッダファイルから”メンバ関数”をそれぞれ抜粋して並べてみたものになります。

※夕顔クラス用の『純粋仮想関数』として「植物クラス」でメンバにしているものが「夕顔クラス」メンバの数と合わないのは、とりあえず”使うもの”だけ選別をしてしまったからです。

抽象…植物クラス(plant.h)

public:
	Plant();
	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(); 

	virtual bool tick_plant() = 0;
	virtual bool throw_away() = 0;

	/*夕顔用の純粋仮想関数********************/
	virtual int get_bloom_count() = 0;
	virtual void bloom_countup() = 0;
	virtual void reset_bloom_count() = 0;
	/* マンドレイク用の純粋仮想関数 ****************************/
	virtual void set_serif(string _serif) = 0;
	virtual string get_serif() = 0;
	virtual void set_voice_size(int _size) = 0;
	virtual int get_voice_size() = 0;
	virtual void limit_countdown() = 0;
	virtual int get_time_limit() = 0;
	virtual int get_max_time_limit() = 0;
	virtual void reset_time_limit() = 0;
	virtual void show_time_limit() = 0;

派生1…夕顔クラス(bottle_gourd.h)

public:
	Bottle_gourd();
	virtual ~Bottle_gourd();
	int get_bloom_count();
	void bloom_countup();
	void reset_bloom_count();
	void info_status();
	bool tick_plant();
	bool throw_away();
	/* マンドレイク用の純粋仮想関数 ****************************/
	void set_serif(string _serif) {}
	string get_serif() { return "";}
	void set_voice_size(int _size) {}
	int get_voice_size() { return 0; }
	void limit_countdown() {}
	int get_time_limit() { return 0; }
	int get_max_time_limit() { return 0; }
	void reset_time_limit() {}
	void show_time_limit() {}

派生2…マンドレイククラス(mandrake.h)

public:
	Mandrake();
	virtual ~Mandrake();
	void set_serif(string _serif);
	string get_serif();
	void set_voice_size(int _size);
	int get_voice_size();
	void limit_countdown();
	int get_time_limit();
	int get_max_time_limit();
	void reset_time_limit();
	void show_time_limit();
	bool tick_plant();
	bool throw_away();
	/**夕顔用*************************/
	int get_bloom_count() {return 0;}
	void bloom_countup() {}
	void reset_bloom_count() {} 

植物クラスの方で純粋関数としてでも、「存在」を示しておかないと、「*基底 = new 派生」というのをやった時に、アクセスできないのでこういう仕様に…。多分、こういうのやるくらいなら最初から「*派生 = new 派生」しておいた方がいいですね。その場合でも、“virtual”と”=0″で抽象クラスの方で『純粋仮想関数』にしてたら未実装対策効きますし。

while文でループさせ、その中を選んだ動作によって分岐処理しています。

それぞれの動作は、それぞれのクラスのメンバ関数の方で定義して、それを呼び出し、もしも、「捨てる」を選んだ場合や「水やりをおろそかして枯らした」場合に、main.cppでグローバル変数として宣言していたフラグ用の変数に”false”を返すようにプログラムしてみました。

植物系のインスタンスにアクセスする前に、「インスタンスが解放済み判断」のため一々フラグ用の変数でif文の分岐をしています。

/***前略****/
bool FLAG_YUGAO = true, FLAG_MANDRAKE = true;
/*****中略*************/
	while (1){
		pM->info_status();// 人間ステータス表示
		if (FLAG_YUGAO) p1->info_status(); 
		if (FLAG_MANDRAKE) p2->info_status();// マンドレイクステータス表示
		
		cout << "どうしますか?" << endl
			<< "1:水をあげる。" << endl
			<< "2:放置する。" << endl
			<< "3:水を溜める。" << endl
			<< "4:捨てる。" << endl
			<< "選択肢を入力してください:";
		cin >> select;
		switch (select) {
		case 1://水やり
			if (FLAG_YUGAO) select = select_plant(); // どちらの植物か選択
			else select = MANDRAKE;

			water = pM->give_water();// 水が足りるかチェック and 足りた場合は与える水の量を返す
			if (water > 0) {
				if (select == YUGAO)p1->watered(water);//水を与える
				if (select == MANDRAKE)p2->watered(water);//水を与える

				cout << "あなたも与えた水の分だけ経験値獲得します。" << endl;
				pM->raise_exp(select); // 水を与えた分だけexpアップ
			}
			break;
		case 2:// 放置
			cout << "放置しました。" << endl;
			break;
		case 3://水を確保し直し
			pM->full_the_water();
			break;
		default://捨てる
			if (FLAG_YUGAO) select = select_plant();// どちらの植物か選択
			else select = MANDRAKE;

			if (select == YUGAO)FLAG_YUGAO = p1->throw_away();
			if (select == MANDRAKE)FLAG_MANDRAKE = p2->throw_away();
			break;
		}
/**以下略****/

そして、それが”false”になった場合に、main関数に戻って、switch文抜けた後に”delete”処理をしています。

             /*前略*/
int main(){
       /**中略**/
		//水を吸収&水の量チェック
		if(FLAG_YUGAO) FLAG_YUGAO = p1->tick_plant();//夕顔
		//if(!FLAG_YUGAO)delete p1;//もし夕顔が枯れたらインスタンス消去
		if(FLAG_MANDRAKE) FLAG_MANDRAKE = p2->tick_plant();//マンドレイク

		if (!FLAG_MANDRAKE) {//マンドレイクフラグが立っていたら
			delete pM;//マスターのインスタンスを消去
			delete p2;//マンドレイクのインスタンス消去
			delete p1;//もし夕顔がまだ存在していたら、消去
			return 0;
		}
		cout << "-----------------------" << endl;
	}
}
/*以下略*/

ちょっと言い訳なんですが、実は、夕顔のインスタンスをターン毎の水の吸収動作の後にすぐにフラグによって”delete”するかのif分岐したかったのです。でも、なぜかそこだけ未定義(?)というのポインタという扱いになって終始謎だったのですが、どうしようもなく、あえなくマンドレイクのインスタンスが消滅した際にまとめて”delete”しています。(どなたか原因教えてくださいm(_ _;)m)

うっかりポインタ渡しでやってしまった失敗メモ

関数の引数に渡した元々のものと、関数内で動いてるものは、別物。渡すものが違っても、所詮は”コピー”。

値渡しは、「”値”そのもののコピー」を渡し、
ポインタ渡しは、「値を格納している場所を示す”アドレス”のコピー」を渡しているだけで、
どっちも、大本ではなく、“コピー”なんですよね。

#include <iostream>

using namespace std;

void func1(int* b) {
	 cout << "(コピーの方) b = " << b << endl;
	 cout << "(コピーの方) &b = " << &b << endl;
	 *b = 2;
	 cout << "(値変更後のコピーの方) *b = " << *b << endl;
	 b = NULL;
	 cout << "(NULLに変更後のコピーの方) b = " << b << endl;
}

int main() {
	int a = 1;
	int* b = &a;
	cout << "(元々の方) b = " << b << endl;
	cout << "(元々の方) &b = " << &b << endl;
	cout << "(元々の方) *b = " << *b << endl;
	func1(b);

	cout << "(元々の方) b = " << b << endl;
	cout << "(元々の方) &b = " << &b << endl;
	cout << "(元々の方) *b = " << *b << endl;
	
	return 0;
}

実行結果↓

(元々の方) b = 0019FCB4
(元々の方) &b = 0019FCA8
(元々の方) *b = 1
(コピーの方) b = 0019FCB4
(コピーの方) &b = 0019FBD4
(値変更後のコピーの方) *b = 2
(NULLに変更後のコピーの方) b = 00000000
(元々の方) b = 0019FCB4
(元々の方) &b = 0019FCA8
(元々の方) *b = 2

「値を格納している場所の”アドレス”のコピー」だから、その”アドレス”から、間接的に大本とも同じ場所にアクセスして”値”をいじれはするけど、その「”コピー”の”アドレス”」を弄っても、大本に影響はない。

関数抜ければ”アドレスのコピー”は消えるし、大本の”アドレス”は元のままだよね!っていう感じなのがいまいち理解できてなくて、やってしまった失敗談です。

実は、上の項のプログラムでは当初、main.cppファイルのmain関数以外の関数に、Plantクラスのインスタンスをポインタ渡しして、その渡した先の関数内で”delete”処理とdeleteした後「アクセス防止・二重消去防止」にアドレスに”NULL”を格納してたのです…。

おまけにそのポインタの格納しているアドレスが、”NULL”かどうかで、ポインタへのアクセスを制限していたのです…。

その結果、“delete”はされているのに、ポインタに格納したはずの”NULL”が反映されておらず、

“delete”したメモリにアクセスしたせいで途中でエラーして停止からやむなくの強制終了
(“NULL”反映されてなきゃ「if(NULL!=ポインタ)」通っちゃうよねorz)

という具合にうっかりメモリリークを量産するという大失態(T_T)

まだ、大規模なもの作ってなくて良かった…のか?(そんなわけない

教訓として、そして誰か私の屍を越えて行って下さる方が、同じような間違いで苦しまないよう、(こんなこと私以外しないか(^_^;))恥ずかしながらも書き残しておきますよ!

あとがき

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

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

Pocket

コメントを残す

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

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