レガシー第2章:クラス化とファイルの分割(アフター:カプセル化の意図)

データと振る舞いを束ね、責務境界を物理ファイルで切り分ける

グローバル変数を全廃!Player(自機)、Invader(敵)、Bullet(弾)をそれぞれヘッダ(.h)と実装(.cpp)に分離。C言語の「構造体ポインタを関数に渡す設計」とC++の「クラス」の決定的な違いと、カプセル化の真の意図を解き明かします。

← 前の章へ進む
次の章へ進む →

2.1 関数設計(C言語)vs クラス設計(C++)の決定的な違い

「C言語でも struct を作ってポインタを渡せば同じじゃないか?」という疑問に、C言語におけるカプセル化の究極技法(不透明ポインタ)と、C++のクラス設計の必然性からお答えします。

🐧 ペンギン先輩
シロクマ指導官!第2章で「クラス」を作るって言ってますけど、C言語でも `struct Player { int x, y; };` って構造体を作って、`Player_Move(&player, -1);` ってポインタを渡せば同じことができますよね? なんでわざわざ C++ の `class` なんて小難しいものを使う必要があるんですか?
🐻‍❄️ シロクマ先生
まさにそこじゃ!その疑問こそ、C言語からC++へ渡る最大の架け橋じゃ。 通常のC言語の構造体はヘッダにメンバが公開されておるから「データが野ざらし」になり、別の不注意な開発者が `player.x = -9999;` と直接代入する事故が防げん。 実はC言語の現場でも、これを防ぐために【不透明ポインタ(Opaque Pointer)】というプロの技法を使っておるのじゃ!
🐧 ペンギン先輩
えっ!?C言語のままでもデータを隠蔽できるんですか!?
🐻‍❄️ シロクマ先生
そうじゃ!ヘッダには `typedef struct Player Player;` と型名だけを前方宣言し、メンバの実体定義は `.c` ファイル内に隠す(Linuxカーネルや標準ライブラリの `FILE*` がまさにそれじゃ)。 しかし、不透明ポインタはヒープ確保(malloc)や間接参照ポインタが必要になる。 C++の `class` の凄さは、**【スタック上にインライン配置してゼロオーバーヘッドの高速性を保ったまま、言語仕様(private)で不正代入を物理的にブロックできる】**点にあるのじゃ!

2.2 なぜヘッダ(.h)と実装(.cpp)に分けるのか?外部仕様書の意図

「ヘッダファイル」は単なるお作法ではありません。C言語の「関数設計書における外部仕様書」そのものです。

🐧 ペンギン先輩
先生、クラスを作るのは分かりましたが、なんで「Player.h」と「Player.cpp」の2つに分けるんですか?1つのファイルの中にクラス宣言も関数の処理も全部書いた方が楽じゃないですか?
🐻‍❄️ シロクマ先生
ペンギン君、君がC言語で書いていた「関数設計書」を思い出してみよ。 設計書には【外部仕様(関数名、引数、戻り値、何ができるか)】と【内部ロジック(アルゴリズムのフローチャート)】があったじゃろ? C++において、「.h」はまさにその【外部仕様書(約束)】、「.cpp」は【内部ロジック】なんじゃ!
### ■ ヘッダと実装を分離する3大設計意図

1. **分割コンパイルとビルド時間の大幅短縮**
- 巨大なゲームプロジェクトでは、全体のビルドに数十分〜数時間かかることがあります。
- もし1ファイルに全て書いていたら、1文字修正しただけで数時間待ちになります。
- `.h`(仕様)と `.cpp`(実装)に分かれていれば、**「変更された .cpp だけ」を数秒で再コンパイル**し、リンカが合体させるだけで済みます。
2. **依存関係の遮断(ブラックボックス化)**
- 他のファイル(例えば main.cpp)は、`Player.h` の取扱説明書を読むだけで自機を操作できます。
- 自機の内部移動アルゴリズムをいくら高速化・改変しても、`Player.h` の関数名が変わらない限り、**main.cpp を再コンパイルする必要すらありません**。
3. **チーム開発における並行作業の実現**
- 先に `Player.h` の仕様(どんな関数を用意するか)さえ決めてコミットすれば、Aさんが `Player.cpp` を実装している間に、Bさんはそれを使って `main.cpp` を書くことができます。

2.3 用意した3つのクラス:内部変数と公開メソッドの全容

2.4 第2章の教材コード:複数ファイル構成プロジェクト一式

ヘッダと実装に分割され、カプセル化された実戦的なコードです。タブを切り替えて各ファイルの役割を確認してください。

📄 Common.h
#pragma once

// ゲーム画面のサイズ定義
const int SCREEN_WIDTH = 30;
const int SCREEN_HEIGHT = 15;
📄 Bullet.h
#pragma once
#include "Common.h"

class Bullet {
private:
    // 【カプセル化】外部からの改ざんを許さない非公開データ
    int m_x;
    int m_y;
    bool m_active;

public:
    Bullet();

    // 弾を発射する
    void spawn(int startX, int startY);

    // 弾を前進させ、画面外で自ら消滅する(責任の自己完結)
    void update();

    // 外部に安全に状態を教えるゲッター関数(const修飾で中身を変えないことを保証)
    bool isActive() const { return m_active; }
    void deactivate() { m_active = false; }
    int getX() const { return m_x; }
    int getY() const { return m_y; }
};
📄 Bullet.cpp
#include "Bullet.h"

Bullet::Bullet() : m_x(0), m_y(0), m_active(false) {}

void Bullet::spawn(int startX, int startY) {
    m_x = startX;
    m_y = startY;
    m_active = true;
}

void Bullet::update() {
    if (!m_active) return;

    m_y--; // 上方向へ移動
    if (m_y < 1) {
        m_active = false; // 天井に達したら自ら消滅
    }
}
📄 Player.h
#pragma once
#include "Bullet.h"

class Player {
private:
    int m_x;
    int m_y;

public:
    Player(int startX, int startY);

    void moveLeft();
    void moveRight();
    void shoot(Bullet& bullet);

    int getX() const { return m_x; }
    int getY() const { return m_y; }
};
📄 Player.cpp
#include "Player.h"
#include "Common.h"

Player::Player(int startX, int startY) : m_x(startX), m_y(startY) {}

void Player::moveLeft() {
    // 境界チェックは自機クラスの責任!
    if (m_x > 1) {
        m_x--;
    }
}

void Player::moveRight() {
    // 壁の手前で自律的にストップ
    if (m_x < SCREEN_WIDTH - 4) {
        m_x++;
    }
}

void Player::shoot(Bullet& bullet) {
    // 弾オブジェクトに「発射せよ」とメッセージを送る
    if (!bullet.isActive()) {
        bullet.spawn(m_x + 1, m_y - 1);
    }
}
📄 Invader.h
#pragma once

class Invader {
private:
    int m_x;
    int m_y;
    bool m_alive;

public:
    Invader(int x, int y);

    void move(int dx, int dy);
    void destroy();

    int getX() const { return m_x; }
    int getY() const { return m_y; }
    bool isAlive() const { return m_alive; }
};
📄 Invader.cpp
#include "Invader.h"

Invader::Invader(int x, int y) : m_x(x), m_y(y), m_alive(true) {}

void Invader::move(int dx, int dy) {
    if (!m_alive) return;
    m_x += dx;
    m_y += dy;
}

void Invader::destroy() {
    m_alive = false;
}
📄 main.cpp
#include <iostream>
#include <windows.h>
#include <conio.h>

#include "Common.h"
#include "Player.h"
#include "Invader.h"
#include "Bullet.h"

void setCursorPosition(int x, int y) {
    COORD coord = { (SHORT)x, (SHORT)y };
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}

int main() {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursorInfo;
    GetConsoleCursorInfo(hConsole, &cursorInfo);
    cursorInfo.bVisible = FALSE;
    SetConsoleCursorInfo(hConsole, &cursorInfo);

    // =========================================================
    // 【オブジェクトの実体化】グローバル変数は完全ゼロ!
    // =========================================================
    Player player(SCREEN_WIDTH / 2 - 1, SCREEN_HEIGHT - 2);
    Bullet bullet;

    const int INVADER_COUNT = 6;
    Invader invaders[INVADER_COUNT] = {
        Invader(4, 2),  Invader(8, 2),  Invader(12, 2),
        Invader(16, 2), Invader(20, 2), Invader(24, 2)
    };

    int invaderDir = 1;
    int moveTimer = 0;
    int score = 0;
    bool isRunning = true;
    bool gameClear = false;

    while (isRunning) {
        // --- 1. 入力受付:オブジェクトにメッセージを送るだけ ---
        if (_kbhit()) {
            char key = _getch();
            if (key == 'a' || key == 'A') player.moveLeft();
            if (key == 'd' || key == 'D') player.moveRight();
            if (key == ' ')               player.shoot(bullet);
            if (key == 'q' || key == 'Q') isRunning = false;
        }

        // --- 2. 弾の自律更新 ---
        bullet.update();

        // --- 3. 敵の移動 ---
        moveTimer++;
        if (moveTimer >= 5) {
            moveTimer = 0;
            bool hitWall = false;
            for (int i = 0; i < INVADER_COUNT; i++) {
                if (!invaders[i].isAlive()) continue;
                if ((invaderDir == 1 && invaders[i].getX() >= SCREEN_WIDTH - 2) ||
                    (invaderDir == -1 && invaders[i].getX() <= 1)) {
                    hitWall = true;
                    break;
                }
            }

            if (hitWall) {
                invaderDir = -invaderDir;
                for (int i = 0; i < INVADER_COUNT; i++) {
                    invaders[i].move(0, 1);
                    if (invaders[i].isAlive() && invaders[i].getY() >= player.getY()) {
                        isRunning = false;
                    }
                }
            } else {
                for (int i = 0; i < INVADER_COUNT; i++) {
                    invaders[i].move(invaderDir, 0);
                }
            }
        }

        // --- 4. 当たり判定(安全なゲッターで問い合わせ) ---
        if (bullet.isActive()) {
            for (int i = 0; i < INVADER_COUNT; i++) {
                if (invaders[i].isAlive() &&
                    bullet.getX() == invaders[i].getX() &&
                    bullet.getY() == invaders[i].getY()) {
                    invaders[i].destroy();
                    bullet.deactivate();
                    score += 100;
                    break;
                }
            }
        }

        // クリア判定
        bool hasAliveInvader = false;
        for (int i = 0; i < INVADER_COUNT; i++) {
            if (invaders[i].isAlive()) { hasAliveInvader = true; break; }
        }
        if (!hasAliveInvader) {
            gameClear = true;
            break;
        }

        // --- 5. 描画処理 ---
        char buffer[SCREEN_HEIGHT][SCREEN_WIDTH];
        for (int y = 0; y < SCREEN_HEIGHT; y++) {
            for (int x = 0; x < SCREEN_WIDTH; x++) {
                buffer[y][x] = (y == 0 || y == SCREEN_HEIGHT - 1 || x == 0 || x == SCREEN_WIDTH - 1) ? '#' : ' ';
            }
        }

        // 各オブジェクトから現在位置を問い合わせて描画
        buffer[player.getY()][player.getX()] = '_';
        buffer[player.getY()][player.getX() + 1] = 'A';
        buffer[player.getY()][player.getX() + 2] = '_';

        if (bullet.isActive() && bullet.getY() > 0 && bullet.getY() < SCREEN_HEIGHT - 1) {
            buffer[bullet.getY()][bullet.getX()] = '|';
        }

        for (int i = 0; i < INVADER_COUNT; i++) {
            if (invaders[i].isAlive()) {
                buffer[invaders[i].getY()][invaders[i].getX()] = 'V';
            }
        }

        setCursorPosition(0, 0);
        for (int y = 0; y < SCREEN_HEIGHT; y++) {
            for (int x = 0; x < SCREEN_WIDTH; x++) {
                std::cout << buffer[y][x];
            }
            std::cout << "\n";
        }
        std::cout << "SCORE: " << score << "  [第2章: クラス設計版]    \n";

        Sleep(33);
    }

    setCursorPosition(0, SCREEN_HEIGHT + 1);
    std::cout << (gameClear ? "=== VICTORY! GAME CLEAR! ===\n" : "=== GAME OVER ===\n");
    return 0;
}

2.5 カプセル化の真意:バグの局所化と防衛的プログラミング

🐧 ペンギン先輩
うわぁ……!main.cpp を見ると、「player.moveLeft()」「player.shoot(bullet)」「bullet.update()」って、まるで英語の命令文を並べているみたいで、何をしているのか一瞬で頭に入ってきます!
🐻‍❄️ シロクマ先生
そうじゃ!main関数は「各オブジェクトに仕事を割り振る指揮官」に専念しておる。 自機の座標計算ロジックはPlayerの中、弾のロジックはBulletの中に閉じ込められておるから、もし「自機が壁を突き抜けるバグ」が出たら、見るべきは Player.cpp のたった10行だけじゃ!何百行ものmain関数を疑う必要は金輪際ないのじゃ!
💡 このセクションの重要ポイント
  • カプセル化の真の目的=バグの局所化: データと処理を1つのクラスに閉じ込めることで、不具合が起きた際の調査対象がクラス内部だけに限定され、デバッグ時間が激減します。
  • ヘッダファイルは外部仕様書(約束): 他人が作ったクラスを使うときは、.cppの中身を読む必要はありません。.hに書かれたpublic関数(外部仕様)だけを見れば安全に使えます。
  • メッセージ送信による協調動作: 手続き型のように外から直接数値を書き換えるのではなく、「動け」「撃て」というメッセージ(関数呼び出し)を送ることで、オブジェクト同士が協調して動作します。

📝 理解度チェッククイズ

Q1. C言語の「構造体を引数で渡す関数設計」に対して、C++の「クラス(カプセル化)」が持つ最大の技術的優位性はどれでしょう?
  • 1. 構造体よりもクラスの方がメモリ使用量が半分になること
  • 2. メンバ変数を private に隠蔽することで、外部コードからの不正な直接代入をコンパイラレベルで完全に防止し、バグの発生領域をクラス内に局所化できること
  • 3. C言語のポインタ演算が使えなくなること
  • 4. 関数名にアンダースコア(_)を使わなくて済むこと
正解と解説を見る
正解:メンバ変数を private に隠蔽することで、外部コードからの不正な直接代入をコンパイラレベルで完全に防止し、バグの発生領域をクラス内に局所化できること
正解です!C言語の構造体は誰でも直接値を書き換えられるため、不正な状態になったときの原因追跡が困難です。C++のクラスは private によるアクセス制御により、クラスの設計者が意図した安全な操作(publicメソッド)以外をコンパイル時に完全に排除できます。
← 前の章へ進む
次の章へ進む →

🐻‍❄️ シロクマC++ラボ インタラクティブ学習システム

ブラウザでJavaScriptを有効にすると、ブラウザ内インベーダーゲームエミュレータ、メモリマップ可視化、UMLクラス図、対話型解説、理解度クイズが起動します。

インタラクティブ学習を開始する →