2026 Security Camp Next N3 C++の”クラス”── なぜ存在するのか, どう設計するのか
C++の”クラス”── なぜ存在するのか, どう設計するのか
対象読者: C++やクラスを触ったことはあるが, 「なぜ必要なのか」がピンとこない人. あるいはクラスを初めて扱う人.
この記事で得られること:
- クラスが存在する理由を, ゼロから理解できる.
- コンストラクタ・メンバ変数・メンバ関数・アクセス修飾子の使いどころがわかる.
- 「クラスをどう設計するか」という基礎的な視点が身につく.
1. クラスがない世界で弾幕を作れるか
私がクラスの話をするときに, 一番解説しやすい弾幕ゲームを作ることにする. まず, 弾1発を管理するコードを書いてみよう.
float bullet_x = 100.0f;
float bullet_y = 200.0f;
float bullet_speed = 5.0f;
bool bullet_alive = true;
1発なら造作なく作ることができる. では100発になったらどうであろうか?
float bullet_x[100];
float bullet_y[100];
float bullet_speed[100];
bool bullet_alive[100];
変数が4種類, それが100個. しかもこれを”同じ添え字”で揃えて管理するのは大変である.
次々に弾幕ができては消えてを繰り返す中で, このコードは管理が非常に煩雑になり, バグの温床になるだろう.
// bullet_x[3] と bullet_alive[3] が”同じ弾”であることは,
// コンパイラやチームで作業している人には伝わらない. 開発現場で怒られるコードとなってしまう.
bullet_x[3] = 320.0f;
bullet_alive[3] = true;
データがバラバラに存在していて, 1つの弾という概念がコードに現れていないのである.
これがクラスを用いずに弾幕を作るのは難しいと判断する理由である.
2. クラスで弾をひとまとめにする
このままだと変数が多く, コードの可読性も落ちるため, クラスを実装する. クラスを使うと,先述の”1つの弾”という概念をコードで表現できる.
class Bullet {
public:
float x, y;
float speed;
bool alive;
};
これで Bullet という型ができた.int や float と同じように使える,自分で定義した型である.
Bullet bullets[100]; // 弾を100発生成する. 見た目がよりよくなった.
bullets[0].x = 100.0f;
bullets[0].alive = true;
// x と alive が同じ弾の要素であることが分かる.
これでバラバラだったデータが,Bullet という一つの箱にまとめられたのだ.
3. 関数もまとめる── メンバ関数
一つの箱にまとめて, ただ弾を生成しただけでは弾幕と呼べない. なぜなら, 弾は毎フレーム動くからである.弾を動かす処理をどこに書くのか?
// クラスの外に書く.
void move_bullet(Bullet& b) {
b.y += b.speed;
}
悪くはないが, Bullet に関係する関数がコードのどこかに散らばっていくことになる.非常に見栄えも悪く, コードの検閲もしづらい. 従って, クラスに関数も一緒に実装すればよい.
class Bullet {
public:
float x, y;
float speed;
bool alive;
void move() { // メンバ関数
y += speed; // 自分自身の y 座標を更新する
}
void destroy() { // メンバ関数は返り値があってもよいが, 今回は省略
alive = false; // 弾を消す.
}
};
bullets[0].move(); // 弾自身に「動け」と命令する.
bullets[0].destroy(); // 弾自身に「消せ」と命令する.
データおよびそのデータを操作する関数がセットで存在している.
これこそクラスの本質であると言っても過言ではない.
4. コンストラクタ── 生成時に必ず初期化をする
弾が動いて消えるようになったが, 今のコードには欠陥がある.
Bullet b;
b.move(); // x や speed が初期化されていないため, ビルドはできても, 未定義動作を引き起こす, 私はこれに2週間悩まされたときがあった.
変数を宣言しただけであって, 中身を決めていないのだ.そのため, 使う前に毎回手動で初期化をする必要が生じる.
Bullet b;
b.x = 320.0f;
b.y = 240.0f;
b.speed = 5.0f;
b.alive = true;
// この初期化を忘れるべからず.
この作業が面倒でなおかつ重要なものであるからおろそかにできない. そこで, コンストラクタを使うと, オブジェクトが生成された瞬間に必ず初期化をするようになる.
class Bullet {
public:
float x, y;
float speed;
bool alive;
// コンストラクタ:クラス名と同じ名前で, 戻り値はない
Bullet(float start_x, float start_y, float spd)
: x(start_x), y(start_y), speed(spd), alive(true) {}
void move() { y += speed; }
void destroy() { alive = false; }
};
Bullet b(320.0f, 240.0f, 5.0f);
// この時点で x, y, speed, alive がすべて確定しているため,
// 初期化忘れが構造的に起きない.
コンストラクタの設計原則:不完全な状態で存在しないようにする
コンストラクタは不完全な状態でオブジェクトを生成できないようにするガードである.
5. private── 外側から壊せない弾を作る
今度こそこれで完璧と思われたが, 問題は常に付きまとうものである. 今の Bullet は外からどんなことでもできてしまう.
Bullet b(320.0f, 240.0f, 5.0f);
b.destroy(); // 弾を消す
b.alive = true; // 弾を消したはずだが直接復活させることができる.
b.speed = -999.0f; // 逆方向に爆速で飛ばす
alive が外から直接書き換えられると, destroy() を呼んだ意味がなくなる. また, 速度も勝手に変更できてしまい, 不都合が生じる.
そこでprivate を使うと, 外からのアクセスを制限することができるのだ.
class Bullet {
private: // ← 外からはアクセスできない
float x, y;
float speed;
bool alive;
// 余談だが, これを忘れてデバッグに数日溶かしたことがある.
public: // ← 外からはアクセス可能
Bullet(float sx, float sy, float spd)
: x(sx), y(sy), speed(spd), alive(true) {}
void move() { y += speed; }
void destroy() { alive = false; }
bool is_alive() { return alive; }
float get_x() { return x; }
float get_y() { return y; }
};
Bullet b(320.0f, 240.0f, 5.0f);
b.destroy();
// b.alive = true; // コンパイルエラー private なのでアクセス不可能である.
なぜ隠すのか?
変えられたくないデータを守るためだけではなく,
正しい手順を踏んでからしか状態を変えられないようにするためである.
つまりdestroy()を経由せずにaliveを変えさせない, という設計の意図をコードで表現している.
6. クラス設計の考え方
ここまでの Bullet を振り返ると, 設計の流れが見えてくる.
ステップ1:何を「管理する」かを決める.
今回であれば
弾 → 位置(x, y), 速度(speed), 生存フラグ(alive)
この4つがあれば問題はないだろう.
ステップ2:何が「必要」かを決める
動く(move), 消える(destroy), 生存確認(is_alive)
これが最低限必要だろう.
ステップ3:何を「隠す」かを決める
外から直接変えてほしくないデータ → private
外から呼んでほしい操作 → public
これを考えるのは大変だったりするが, 後のことを考えるとしっかりすべき.
この順序で考えると, クラスは自然に設計できる.
7. まとめ
class Bullet {
private:
float x, y, speed;
bool alive;
public:
Bullet(float sx, float sy, float spd)
: x(sx), y(sy), speed(spd), alive(true) {}
void move() { y += speed; }
void destroy() { alive = false; }
bool is_alive() { return alive; }
float get_x() { return x; }
float get_y() { return y; }
};
| 機能 | 役割 |
|---|---|
| メンバ変数 | 弾に関係するデータをひとまとめにする |
| メンバ関数 | そのデータを操作する手順を一緒に置く |
| コンストラクタ | 生成時に必ず初期化をする |
private |
正しい手順でのみ状態を変えられるようにする |
クラスは「データのまとまりを型として定義する」機能である.
「何をまとめ, 何を隠し, どこまでアクセス可能か」──この3つを考えることがクラス設計の本質になると私は考える.
次に調べると面白いこと:
- 継承──
Bulletを親に, 種類ごとの弾を子クラスにする設計へ -
protectedとprivateの使い分け──子クラスにどこまで見せるか - デストラクタ──オブジェクトが消えるときに何が起きるか
-
std::unique_ptr──生ポインタを使わない現代的な C++ への第一歩
参考資料
- cppreference.com - Classes
- cppreference.com - virtual function specifier
- Bjarne Stroustrup, The C++ Programming Language (4th edition)
- C++を使ってシューティングを作った話
- DXライブラリ ゲームプログラム講座
Discussion