C++/オブジェクト指向をRPGで説明する回
オブジェクト指向とは
オブジェクトとは、そのまま「もの」という意味です。
ゲームで例えると、人間、敵、装備品、快復アイテムなど、すべてものですので、それらはすべてオブジェクトとなります。
オブジェクト指向とは、オブジェクトを中心に考えることです。
例えば、RPGなどで、人間は名前を持ち、座右の銘を持ち、そして戦ったり、眠ることができます。
このように、オブジェクトが どういうデータを持っているか(名前、座右の銘など)、どういう操作ができるか(戦う、眠るなど)をまとめたものがオブジェクト指向で大切なのです。
- オブジェクトは、「もの」と表せるもの
- オブジェクト指向は、オブジェクトを中心に考えることである
- オブジェクトがどういうデータを持っているか、どういう操作ができるかをまとめたものがオブジェクト指向で大切である
RPGを作ってみよう
RPGの計画書
RPGに登場するオブジェクト
【人間】
◆どういうデータを持っているか
・名前
・座右の銘
・体力
・最大体力
・レベル
・経験値
・お金
・アイテム所持
・装備
◆どういう操作ができるか
・攻撃
・睡眠
・アイテムを使う
・装備をつける
・経験値をゲットする
・発言
【敵】
◆どういうデータを持っているか
・名前
・体力
・経験値
・お金
・アイテム所持
◆どういう操作ができるか
・攻撃
経験値との相関関係
経験値が上がれば、レベルが上がり、
レベルが上がれば、最大HPが上がるようにします。
最大HPの設定
Lv.1→最大HP:10
Lv.2→最大HP:15
Lv.3→最大HP:20
Lv.4→最大HP:25
Lv.5→最大HP:30
最大HPはレベル1上がるごと
に5だけ増加します。
経験値とレベルの相関関係は次のようにします。
とりあえず構造体で作ってみる
部分的でいいから。
#include <iostream> #include <string> #include <math.h> using namespace std; struct Human { /* どういうデータがあるか */ string name; // 名前 string motto; // 座右の銘 int HP; // 体力 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル int EXP; // 経験値 int money; // お金 }; // 初期化 void init(Human& human) { human.name = ""; human.motto = ""; human.HP = 10; human.maxHP = 10; human.level = 1; human.EXP = 0; human.money = 0; } // キャラクターの状態を表示 void info(Human human) { cout << "◆名前:" << human.name << "◆座右の銘:" << human.motto << "◆体力:" << human.HP << "/" << human.maxHP << "◆レベル:" << human.level << "◆経験値:" << human.EXP << "◆所持金:" << human.money << endl; } // 発言 void say(Human human, string str) { cout << human.name << "「" << str << "」" << endl; } int main() { // 宣言 Human player; // 初期化 init(player); // 名前の設定 player.name = "コウちゃん"; // キャラクターの状態を表示 info(player); // 発言 say(player, "せっかくだから、俺はこの赤の扉を選ぶぜ!"); return 0; }
クラスを使って作ってみる
#include <iostream> #include <string> using namespace std; class Human { public: /* どういうデータがあるか */ string name; // 名前 string motto; // 座右の銘 int HP; // 体力 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル int EXP; // 経験値 int money; // お金 // コンストラクタ(初期化) Human() { name = ""; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } }; int main() { // 宣言&初期化 Human player; // 名前の設定 player.name = "コウちゃん"; // キャラクターの状態を表示 player.info(); // 発言 player.say("せっかくだから、俺はこの赤の扉を選ぶぜ!"); return 0; }
struct
がclass
になっているところと、class
だと「どういうデータを持っているか」と「どういう操作ができるか」がどっちもまとまっているのよ。
はい、おちゃっぱちゃんの言う通り、C言語の構造体ではデータしかまとめられませんが、C++のクラスはデータと動作をまとめることができるのです。 具体的にRPGで例えるとこんな感じです。
class
で宣言した実体のことをインスタンスといいます。
例えば、
Human player
とした場合、Human
がクラスという「人間の設計書」で、それを元に作られたオブジェクトがplayer
というインスタンス、ということです。
クラスにはクラス名と同じ名前の特別なメンバ関数が2つあります。
今回の場合は、Human
ですので、Human()
と~Human()
がそれにあたります。
Human()
はインスタンスが生成された直後に呼び出されて処理されます。いわゆる初期化と同じです。このメンバ関数はコンストラクタといいます。
~Human()
は逆にインスタンスが破棄されるときに呼び出されて処理されます。このメンバ関数をデストラクタといいます。
これによってHuman player
という宣言がされた瞬間にHuman()
が呼び出されて、初期化されるのです。
- クラスはデータと動作をまとめることができる
- クラスの中にある関数をメンバ関数という
- クラスで宣言した実体のことをインスタンスという
- コンストラクタはインスタンスが生成されるときに呼び出されて処理されるメンバ関数である
- デストラクタはインスタンスが破棄されるときに呼び出されて処理されるメンバ関数である
thisと関数のオーバーロード
おちゃっぱちゃんが書き換えたソースコードをさらに書き換えてみましょう。
#include <iostream> #include <string> using namespace std; class Human { public: /* どういうデータがあるか */ string name; // 名前 string motto; // 座右の銘 int HP; // 体力 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル int EXP; // 経験値 int money; // お金 // コンストラクタ(初期化) Human() { name = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // コンストラクタ(初期化) Human(string name) { this->name = name; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } }; int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); // キャラクターの状態を表示 player.info(); // 発言 player.say("せっかくだから、俺はこの赤の扉を選ぶぜ!"); return 0; }
今度はコンストラクタがふたつになっていますね。
実は、C++では関数名が同じでも、引数の型や数が異なっていれば、複数宣言することができます。
このことを関数のオーバーロードといいます。
2つ目のコンストラクタにthis
というキーワードがあります。
このthis
は、生成するインスタンス自身を格納する特別な変数です。
Human(string name)
の仮引数は何?
string
型のname
という引数だけど、
player
というインスタンスはどういうデータを持ってる?
name
とかmotto
とか。
Human(string name)
の中ではname
って言ったら、仮引数のname
になるでしょう?
player
が持っているデータのname
を呼び出せないよね。だからthis
というのを使えば、player
が持っているデータのname
を呼び出せるようにしたんだ。
不正な書き込みを禁止する
int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); // キャラクターの状態を表示 player.info(); // 発言 player.say("せっかくだから、俺はこの赤の扉を選ぶぜ!"); // キャラクターの状態をメチャクチャにする player.HP = 1000; player.level = 100; player.money = 100; // キャラクターの状態を表示 player.info(); return 0; }
#include <iostream> #include <string> using namespace std; class Human { private: /* どういうデータがあるか */ string name; // 名前 string motto; // 座右の銘 int HP; // 体力 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル int EXP; // 経験値 int money; // お金 public: // コンストラクタ(初期化) Human() { name = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // コンストラクタ(初期化) Human(string name) { this->name = name; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } }; int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); // キャラクターの状態を表示 player.info(); // 発言 player.say("せっかくだから、俺はこの赤の扉を選ぶぜ!"); // キャラクターの状態をメチャクチャにする player.HP = 1000; //compile error player.level = 100; //compile error player.money = 100; //compile error // キャラクターの状態を表示 player.info(); return 0; }
アクセシビリティとは変数やメンバ関数のアクセス設定のことを示します。
クラス内部でpublic:
と書かれた場所より下に宣言された変数やメンバ関数は、どこからでもアクセス可能になりますが、
private:
と書かれた場所より下に宣言された変数やメンバ関数は、クラス内部からしかアクセスできなくなります。
もし、アクセシビリティがprivate
になっている変数やメンバ関数にクラス外部からアクセスしようとすると、コンパイルエラーになります。
public
なメンバ関数を介して間接的に変更するんだ。
#include <iostream> #include <string> using namespace std; class Human { private: /* どういうデータがあるか */ string name; // 名前 string motto; // 座右の銘 int HP; // 体力 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル int EXP; // 経験値 int money; // お金 public: // コンストラクタ(初期化) Human() { name = ""; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // コンストラクタ(初期化) Human(string name) { this->name = name; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } // 座右の銘の設定(セッター) void set_motto(string motto) { this->motto = motto; } // 座右の銘の取得(ゲッター) string get_motto(void) { return this->motto; } // 経験値をゲットする void add_EXP(int EXP) { if (EXP < 0) return; this->EXP += EXP; } }; int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); // 座右の銘の設定 player.set_motto("せっかくだから、俺はこの赤の扉を選ぶぜ!"); // キャラクターの状態を表示 player.info(); // 座右の銘の取得 cout << "■座右の銘:" << player.get_motto() << endl; return 0; }
set_motto
のようにprivate
な変数に格納されているデータをメンバ関数越しに設定するためにある、メンバ関数のことをセッター(setter)といいます。
また、get_motto
のようにprivate
な変数に格納されているデータをメンバ関数越しに取得するためにある、メンバ関数のことをゲッター(getter)といいます。
#include <iostream> #include <string> #include <math.h> using namespace std; class Human { private: /* どういうデータがあるか */ string name; // 名前 string motto; // 座右の銘 int HP; // 体力 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル int EXP; // 経験値 int money; // お金 public: // コンストラクタ(初期化) Human() { name = ""; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // コンストラクタ(初期化) Human(string name) { this->name = name; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } // 座右の銘の設定(セッター) void set_motto(string motto) { this->motto = motto; } // 座右の銘の取得(ゲーター) string get_motto(void) { return this->motto; } // 経験値をゲットする void add_EXP(int EXP) { if (EXP < 0) return; this->EXP += EXP; calc_level(); } // レベルの計算 void calc_level(void) { level = sqrt(2.0*(EXP+1.0)/3.0+289.0/36.0)-17.0/6.0 + 1.0; maxHP = 5 + 5 * level; } // 次のレベルに上がるための計算 void calc_next_level(void) { int needEXP = (3.0*level*level/2.0 + 17.0*level/2.0) - EXP; cout << "次のレベルに上がるまであと" << needEXP << "の経験値が必要です。" << endl; } }; int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); // キャラクターの状態を表示 player.info(); // 次のレベルに上がるための計算 player.calc_next_level(); // 経験値をゲットする player.add_EXP(10); // キャラクターの状態を表示 player.info(); // 次のレベルに上がるための計算 player.calc_next_level(); return 0; }
- クラス内部へのアクセスの制限度合いをアクセシビリティという
- クラス内部に勝手にアクセスしないようにすることを隠蔽化という
public
はどこからでもアクセス可能であるprivate
はクラス内部のみアクセス可能である
継承について
class Enemy { private: string name; int HP; int EXP; int money; };
敵クラスを宣言すると、こんな感じになりますが、 よく見ると人間クラスと共通点がいくつかあります。
#include <iostream> #include <string> #include <math.h> using namespace std; // 生物クラス(基底クラス) class Creature { protected: string name; // 名前 int HP; // 体力 int EXP; // 経験値 int money; // お金 public: // 死んでいるか判定(trueなら死んでいる。falseなら生きている。) bool is_dead(){ return (HP==0)?true:false; } // ダメージを受ける void damage(int point) { HP -= point; cout << name << "はダメージ" << point << "を食らった!" << endl; if (HP<0) { HP=0; cout << name << "は死んでしまった…!死んでしまうとは情けない" << endl; } } }; // 人間クラス class Human : public Creature{ private: /* どういうデータがあるか */ string motto; // 座右の銘 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル public: // コンストラクタ(初期化) Human() { name = ""; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // コンストラクタ(初期化) Human(string name) { this->name = name; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } // 座右の銘の設定(セッター) void set_motto(string motto) { this->motto = motto; } // 座右の銘の取得(ゲーター) string get_motto(void) { return this->motto; } // 経験値をゲットする void add_EXP(int EXP) { if (EXP < 0) return; this->EXP += EXP; calc_level(); } // レベルの計算 void calc_level(void) { level = sqrt(2.0*(EXP+1.0)/3.0+289.0/36.0)-17.0/6.0 + 1.0; maxHP = 5 + 5 * level; } // 次のレベルに上がるための計算 void calc_next_level(void) { int needEXP = (3.0*level*level/2.0 + 17.0*level/2.0) - EXP; cout << "次のレベルに上がるまであと" << needEXP << "の経験値が必要です。" << endl; } // 寝る void sleep() { HP = maxHP; cout << name << "は睡眠を取った。zzZ..." << endl; } }; // 敵クラス class Enemy : public Creature { private: string name; int HP; int EXP; int money; public: }; int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); // キャラクターの状態を表示 player.info(); // キャラクターにダメージ5 player.damage(5); // キャラクターの状態を表示 player.info(); // キャラクターが寝る player.sleep(); // キャラクターの状態を表示 player.info(); // 次のレベルに上がるための計算 player.calc_next_level(); return 0; }
クラスを継承する側を子クラスとか派生クラス、クラスを継承される側を親クラスとか基底クラスといいます。
C++ではクラスを継承するときclass 子クラス : public 親クラス
というふうにクラスを宣言することによって継承することができます。
#include <iostream> #include <string> #include <math.h> using namespace std; // 生物クラス(基底クラス) class Creature { protected: string name; // 名前 int HP; // 体力 int EXP; // 経験値 int money; // お金 public: // 死んでいるか判定(trueなら死んでいる。falseなら生きている。) bool is_dead(){ return (HP==0)?true:false; } // ダメージを受ける void damage(int point) { HP -= point; cout << name << "はダメージ" << point << "を食らった!" << endl; if (HP <= 0) { HP = 0; cout << name << "は死んでしまった…!死んでしまうとは情けない" << endl; } } // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆体力:" << HP << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // ゲッターの一覧 string get_name() {return name;} int get_HP() {return HP;} int get_EXP() {return EXP;} int get_money() {return money;} }; // 人間クラス class Human : public Creature{ private: /* どういうデータがあるか */ string motto; // 座右の銘 int maxHP; // 最大の体力(レベルによって変わる) int level; // レベル public: // コンストラクタ(初期化) Human() { name = ""; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // コンストラクタ(初期化) Human(string name) { this->name = name; motto = ""; HP = 10; maxHP = 10; level = 1; EXP = 0; money = 0; } // デストラクタ ~Human() { } /* どういう操作ができるか */ // キャラクターの状態を表示 void info() { cout << "◆名前:" << name << "◆座右の銘:" << motto << "◆体力:" << HP << "/" << maxHP << "◆レベル:" << level << "◆経験値:" << EXP << "◆所持金:" << money << endl; } // 発言 void say(string str) { cout << name << "「" << str << "」" << endl; } // 座右の銘の設定(セッター) void set_motto(string motto) { this->motto = motto; } // 座右の銘の取得(ゲーター) string get_motto(void) { return this->motto; } // 経験値をゲットする void add_EXP(int EXP) { if (EXP < 0) return; this->EXP += EXP; calc_level(); } // レベルの計算 void calc_level(void) { level = sqrt(2.0*(EXP+1.0)/3.0+289.0/36.0)-17.0/6.0 + 1.0; maxHP = 5 + 5 * level; } // 次のレベルに上がるための計算 void calc_next_level(void) { int needEXP = (3.0*level*level/2.0 + 17.0*level/2.0) - EXP; cout << "次のレベルに上がるまであと" << needEXP << "の経験値が必要です。" << endl; } // 寝る void sleep() { HP = maxHP; cout << name << "は睡眠を取った。zzZ..." << endl; } //攻撃 void attack(Creature& creature) { cout << name << "は" << creature.get_name() << "にダメージ" << level << "を与えた。" << endl; creature.damage(level); } //敵の経験値やお金を押収 void seizure(Creature& creature) { money += creature.get_money(); EXP += creature.get_EXP(); cout << name << "は" << creature.get_money() << "Gを手に入れた" << endl; cout << name << "は" << creature.get_EXP() << "EXPを手に入れた" << endl; } }; // 敵クラス class Enemy : public Creature { protected: int attack_power; // 攻撃力 public: // 攻撃 void attack(Creature& creature) { cout << name << "は" << creature.get_name() << "にダメージ" << attack_power << "を与えた。" << endl; creature.damage(attack_power); } }; // ぞぬ(雑魚キャラ) class Zonu : public Enemy { public: Zonu() { name = "ぞぬ"; HP = 5; EXP = 5; money = 1; attack_power = 1; } }; // わぬ(ボスキャラ) class Wanu : public Enemy { public: Wanu() { name = "わぬ"; HP = 100; EXP = 100; money = 20; attack_power = 10; } }; int main() { // 宣言&初期化(名前の設定) Human player("コウちゃん"); Zonu zonu; while (1) { // プレイヤーの情報 player.info(); // 敵の情報 zonu.info(); // プレイヤーの攻撃 player.attack(zonu); // 敵が死んだら終わり if (zonu.is_dead()) break; // プレイヤーの情報 player.info(); // 敵の情報 zonu.info(); // 敵の攻撃 zonu.attack(player); // プレイヤーが死んだら終わり if (player.is_dead()) break; } // 敵の経験値やお金を回収 player.seizure(zonu); // プレイヤーの情報 player.info(); return 0; }
- あるクラスの操作やデータを別のクラスに引き継ぐことを継承という
- クラスを継承する側を子クラスまたは派生クラスという
- クラスを継承される側を親クラスまたは基底クラスという
class 子クラス : public 親クラス
でクラスを継承する
最後に
これでオブジェクト指向の説明は終わりです。
お疲れ様でした。