C++超入門⑨ クラスの基本を知ろう
class、オブジェクト、public、private、コンストラクタ、メンバー関数を使い、データと処理を一つにまとめます。
構造体でプレイヤーを作れたとして、どこからでもhp = -100;と変更できるのは困りますよね。HPは0以上、ダメージが負なら受け付けない、といったルールを守りたいところです。
クラスを使うと、データと、そのデータを操作する関数をまとめられます。今回は「難しい設計図」ではなく、値を変える入口を決める道具として学びましょう。
C++17以降で試します。構造体と参照が分かっていることを前提にします。
1. まずは、HPを操作できるPlayerを作る
#include <iostream>
#include <string>
class Player {
public:
Player(const std::string& initialName)
: name(initialName) {
}
void takeDamage(int damage) {
if (damage <= 0) {
return;
}
if (damage >= hp) {
hp = 0;
} else {
hp -= damage;
}
}
void show() const {
std::cout << name << ": HP " << hp << '\n';
}
private:
std::string name;
int hp = 100;
};
int main() {
Player hero("Aoi");
hero.show();
hero.takeDamage(30);
hero.show();
hero.takeDamage(200);
hero.show();
return 0;
}
Aoi: HP 100
Aoi: HP 70
Aoi: HP 0
最初はコード全体を1つのファイルで試してください。Playerの定義とmainの両方が必要です。
2. クラスとオブジェクトを分けて考える
class Player { ... };がクラス、Player hero("Aoi");で作った実際の値がオブジェクトです。
「クラスが型、オブジェクトがその型の実物」という関係です。Player partner("Ren");とすれば、別のプレイヤーができます。
heroへダメージを与えても、partnerのHPは変わりません。それぞれが自分のnameとhpを持っているからです。
クラス内のtakeDamageやshowをメンバー関数と呼びます。hero.show()のように、どのオブジェクトの処理を呼ぶかをドットで指定します。
3. publicは入口、privateは内部のデータ
| 指定 | 今回の役割 |
|---|---|
public |
外から呼べる操作 |
private |
外から直接触らせないデータ |
mainではhero.takeDamage(30);を呼べますが、hero.hp = -100;はアクセスできないためコンパイルエラーになります。
このように、内部の表現を隠して操作方法を決める考え方をカプセル化と呼びます。用語だけだと難しそうですが、「決めた窓口から値を変更する」と考えると近いです。
ただし、privateはセキュリティ上の暗号化ではありません。C++のコードの構造としてアクセスを制限する機能です。特別に許可するfriendなどの仕組みもありますが、今回は使いません。
privateだけで、不正な値を防げるわけではない
privateにしても、メンバー関数の中で無条件にhp = -100と書けば不正な状態になります。入口の処理が正しいことも必要です。
今回のtakeDamageは、0以下を拒否し、HP以上のダメージでは0、そうでなければ引き算としています。これで初期値100から、HPが0〜100の範囲に保たれます。
4. ダメージの処理を値の変化で追う
hero.takeDamage(30);は、heroのHP100へ30のダメージを与える呼び出しです。
- 30は0以下ではないので、続行する
- 30は現在のHP100以上ではない
hp -= damageで100 - 30を計算し、70になるshowが70を表示する
その後に200を渡すと、200はHP70以上なのでhp = 0へ進みます。
負の値を渡したらどうでしょう。takeDamage(-20)は最初の条件で終了します。単純にhp -= damageだけだと、負のダメージでHPが増えてしまうためです。
「入力する値の意味」と「計算できるか」は別です。整数として計算できても、ゲームのルールに合うとは限りません。
5. コンストラクタは最初の状態を作る
Player(const std::string& initialName)
: name(initialName) {
}
クラス名と同じ名前の、この特別な関数がコンストラクタです。オブジェクトを作るときに呼ばれます。
通常の関数と違い、intやvoidのような戻り値の型を書きません。
: name(initialName)はメンバー初期化子リストです。nameを先に作ってから代入するのではなく、initialNameの値で初期化しています。HPにはメンバー宣言時の100が使われます。
このコンストラクタなら名前が必要なので、Player hero;とは書けません。Player hero("Aoi");のように、必要な初期情報を渡します。
Microsoftのコンストラクタの説明でも、初期化と通常のメンバー関数の違いを確認できます。
6. showの後ろのconstは何?
void show() constのconstは、「この関数では、そのオブジェクトの通常のデータメンバーを書き換えない」という指定です。
画面表示はHPを減らす操作ではないので、showに付けています。引数のconst std::string&とは位置が違い、こちらは関数が操作するオブジェクトに関する約束です。
この約束があると、constなPlayerに対してもshowを呼べます。読み取り用の処理と変更用の処理が、宣言から見分けやすくなります。
7. structでも関数やprivateは使える
「structはデータだけ、classは処理を入れられる」と覚えると、あとで混乱します。C++のstructにもメンバー関数やprivateを置けます。
主な言語上の違いは、何も指定しない場合のメンバーと継承のアクセスです。structはpublic、classはprivateになります。C++のアクセス制御の仕様に定義されています。
単純なデータの集まりを素直に公開するならstruct、値を変えるルールを守りたいならclass、という使い分けが一般的です。クラスにすれば自動で「よい設計」になるわけではありません。
8. 練習:残高を持つBankAccountを作ろう
学習用の残高管理です。実際の銀行システムを作る例ではありません。
次の条件を満たすクラスを作ってください。
- privateに残高を持ち、初期値は0円
- publicのdepositで入金できる
- 0以下の入金は拒否する
- 残高の上限を10000円にする
- showで残高を表示する
模範解答
#include <iostream>
class BankAccount {
public:
bool deposit(int amount) {
if (amount <= 0 || amount > MAX_BALANCE - balance) {
return false;
}
balance += amount;
return true;
}
void show() const {
std::cout << "残高: " << balance << "円\n";
}
private:
static constexpr int MAX_BALANCE = 10000;
int balance = 0;
};
int main() {
BankAccount account;
if (!account.deposit(3000)) {
std::cout << "入金できませんでした。\n";
}
if (!account.deposit(-500)) {
std::cout << "入金できませんでした。\n";
}
if (!account.deposit(8000)) {
std::cout << "上限を超えるため入金できません。\n";
}
account.show();
return 0;
}
解説
3000円は受け付けます。-500円は0以下なので拒否。次の8000円は残高3000円と合わせると11000円になるため拒否し、最後の残高は3000円です。
depositは成功ならtrue、拒否ならfalseを返します。呼び出す側は、戻り値を使ってメッセージを変えられます。
MAX_BALANCE - balanceで残りの容量を先に計算しているので、巨大な入金額を加えてから上限を調べる形になりません。符号付き整数のオーバーフローは、足した後に確認しても遅い場合があります。
static constexprは、このクラスで共有するコンパイル時定数を作る指定です。ここでは「すべての口座で使う上限」として読みましょう。
次の一歩
クラスを書くときは、まず「どんな状態を守りたいか」を決めます。HPなら0以上、今回の残高なら0〜10000円。その条件を保つ操作だけをpublicへ出すと、役割がはっきりします。