レガシー第3章:オブジェクトの動的生成と寿命管理(可変エフェクトとメモリ設計)

ヒープ断片化を防ぐ「静的オブジェクトプール」の正解と、動的メモリ寿命管理の深淵

敵の撃破エフェクト(Particle)を題材に、組込み・リアルタイムゲーム開発現場で動的メモリ(malloc/new)が忌避される理由と、ゼロ・アロケーションを実現する「静的オブジェクトプール設計(レガシーの正解)」を徹底解剖。さらに、メモリ制約のない汎用PC環境で使われる std::vector(参考資料)との対比を通じて、真のメモリ設計力を養います。

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

3.1 なぜ現場は動的メモリを恐れるのか?「静的プール(レガシーの正解)」vs「動的確保」

組込み機器、車載ECU、航空宇宙、リアルタイムゲームエンジンなど、止まることが許されない現場では「動的メモリ確保(malloc/new)の実行時呼び出しは原則禁止」という鉄の掟が存在します。レガシー環境における真の正解とは何かを学びます。

🐧 ペンギン先輩
シロクマ指導官!第2章でクラス化はできましたが、敵を倒した瞬間が「ポッ」と消えるだけでなんだか地味です……もっと弾が当たったときにドカーン!と火花が散る派手な演出が欲しいです!
🐻‍❄️ シロクマ先生
いい着眼点じゃ!ゲームの爽快感を決定づける要素、それが『パーティクル(粒子エフェクト)』じゃ。敵が撃破された瞬間、10個以上の火花オブジェクトを一気に生成して四方へ飛ばすのじゃ!
🐧 ペンギン先輩
ええっ!?でも可変個数のオブジェクトを作るとなると、毎フレーム `malloc` とかやるんですよね? 僕、どこで `free()` を呼べばいいか分からなくなって、メモリリークでOSをクラッシュさせたり、二重解放で即死セグフォ食らったりしてトラウマなんですが……!
🐻‍❄️ シロクマ先生
まさにそこが現場の分水嶺じゃ!実は航空宇宙、車載(MISRA規格)、格闘ゲーム等のリアルタイム制御の現場では、**『プログラム実行中の malloc / new の呼び出しは全面禁止』**とされることが多いのじゃ! なぜなら、メモリの断片化(Heap Fragmentation)で突然メモリ確保に失敗したり、OSヒープの内部ロックでフレーム落ちが発生するからじゃ!
🐧 ペンギン先輩
えっ!?mallocもnewも使えないなら、どうやって火花を飛ばすんですか!?
🐻‍❄️ シロクマ先生
それこそがレガシー現場の黄金律、**【静的オブジェクトプール(Static Object Pool)】**じゃ! 最大出現数(例: 64個)の配列をあらかじめ静的領域に確保しておき、空いているスロットを使い回す。ヒープ確保ゼロ、断片化ゼロ、処理速度一定(O(1))の究極の安定性を誇る設計じゃ! この「レガシーの正解」を学んだ上で、汎用PC開発で使われるモダンな `std::vector`(参考資料)との違いを見ていくのじゃ!

3.2 メモリの可視化:スタックとヒープにおけるオブジェクトの命の環

「型(クラス)」はたい焼きの金型に過ぎません。プログラム実行中、メモリ上に「実体(インスタンス)」がどうクローンされ、いつ生まれてどう消えるのかを図解します。

3.3 新設クラスの設計:Particle と Game の内部構造

3.4 第3章の教材コード:動的生成と寿命管理の完全実装

弾が敵に当たった瞬間に火花が咲き誇り、寿命が尽きると安全にメモリから消え去る、オブジェクト指向の完成形コードです。

📄 Particle.h
#pragma once
#include "Common.h"

class Particle {
private:
    float m_x;
    float m_y;
    float m_vx;     // X方向速度
    float m_vy;     // Y方向速度
    int m_lifetime; // 残り寿命(フレーム数)
    char m_glyph;   // 表示文字 ('*', '+', '.')

public:
    Particle(float x, float y, float vx, float vy, int lifetime, char glyph);

    // 毎フレーム座標を動かし、寿命を減らす
    void update();

    // 寿命が尽きたか(消滅判定)
    bool isDead() const { return m_lifetime <= 0; }

    int getX() const { return (int)m_x; }
    int getY() const { return (int)m_y; }
    char getGlyph() const { return m_glyph; }
};
📄 Particle.cpp
#include "Particle.h"

Particle::Particle(float x, float y, float vx, float vy, int lifetime, char glyph)
    : m_x(x), m_y(y), m_vx(vx), m_vy(vy), m_lifetime(lifetime), m_glyph(glyph) {}

void Particle::update() {
    m_x += m_vx;
    m_y += m_vy;
    m_lifetime--; // 1フレームごとに寿命を消費(老化プロセス)
}
📄 Game.h
#pragma once
#include <vector>
#include "Common.h"
#include "Player.h"
#include "Invader.h"
#include "Bullet.h"
#include "Particle.h"

class Game {
private:
    Player m_player;
    std::vector<Bullet> m_bullets;       // 動的な弾リスト(連射対応)
    std::vector<Invader> m_invaders;     // 動的な敵リスト
    std::vector<Particle> m_particles;   // 動的な火花パーティクルリスト!

    int m_score;
    int m_invaderDir;
    int m_invaderMoveTimer;
    bool m_isRunning;
    bool m_gameClear;

    // 爆発火花をヒープ上に動的生成するファクトリメソッド
    void spawnExplosion(int x, int y);
    void processInput();
    void update();
    void render();

public:
    Game();
    void run(); // ゲーム全体の起動窓口
};
📄 Game.cpp
#include "Game.h"
#include <iostream>
#include <algorithm>
#include <cmath>
#include <windows.h>
#include <conio.h>

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

Game::Game() 
    : m_player(SCREEN_WIDTH / 2 - 1, SCREEN_HEIGHT - 2),
      m_score(0), m_invaderDir(1), m_invaderMoveTimer(0),
      m_isRunning(true), m_gameClear(false) {
    
    // 敵を動的に配置
    for (int col = 0; col < 6; col++) {
        m_invaders.push_back(Invader(4 + col * 4, 2));
    }
}

// =============================================================
// 【動的生成フェーズ】弾が命中した瞬間、10個の火花実体を生み出す
// =============================================================
void Game::spawnExplosion(int x, int y) {
    const char glyphs[] = { '*', '+', '.', 'x', '#' };
    for (int i = 0; i < 10; i++) {
        // 360度放射状のベクトルを計算
        float angle = (float)(i * (3.1415926 * 2.0 / 10.0));
        float speed = 0.5f + (float)(rand() % 5) * 0.1f;
        float vx = std::cos(angle) * speed * 1.5f;
        float vy = std::sin(angle) * speed * 0.8f;
        int life = 4 + rand() % 5; // 4〜8フレームの寿命
        char g = glyphs[rand() % 5];

        // vectorのメモリ上に直接Particleインスタンスを追加(生成)
        m_particles.push_back(Particle((float)x, (float)y, vx, vy, life, g));
    }
}

void Game::processInput() {
    if (_kbhit()) {
        char key = _getch();
        if (key == 'a' || key == 'A') m_player.moveLeft();
        if (key == 'd' || key == 'D') m_player.moveRight();
        if (key == ' ') {
            // 連射制限:画面内に最大3発まで動的追加
            if (m_bullets.size() < 3) {
                m_bullets.push_back(Bullet(m_player.getX() + 1, m_player.getY() - 1));
            }
        }
        if (key == 'q' || key == 'Q') m_isRunning = false;
    }
}

// 【C++03 述語関数】erase-remove で使用する消去条件の判定関数
static bool isParticleDead(const Particle& p) { return p.isDead(); }
static bool isBulletInactive(const Bullet& b) { return !b.isActive(); }
static bool isInvaderDead(const Invader& inv) { return !inv.isAlive(); }

void Game::update() {
    // 1. 弾の更新
    for (size_t i = 0; i < m_bullets.size(); ++i) {
        m_bullets[i].update();
    }

    // 2. 敵の更新(タイマー制御)
    m_invaderMoveTimer++;
    if (m_invaderMoveTimer >= 5) {
        m_invaderMoveTimer = 0;
        bool hitWall = false;
        for (size_t i = 0; i < m_invaders.size(); ++i) {
            if ((m_invaderDir == 1 && m_invaders[i].getX() >= SCREEN_WIDTH - 2) ||
                (m_invaderDir == -1 && m_invaders[i].getX() <= 1)) {
                hitWall = true;
                break;
            }
        }
        if (hitWall) {
            m_invaderDir = -m_invaderDir;
            for (size_t i = 0; i < m_invaders.size(); ++i) {
                m_invaders[i].move(0, 1);
                if (m_invaders[i].getY() >= m_player.getY()) {
                    m_isRunning = false;
                }
            }
        } else {
            for (size_t i = 0; i < m_invaders.size(); ++i) {
                m_invaders[i].move(m_invaderDir, 0);
            }
        }
    }

    // 3. 当たり判定(Bullet vs Invader)
    for (size_t b = 0; b < m_bullets.size(); ++b) {
        if (!m_bullets[b].isActive()) continue;
        for (size_t inv = 0; inv < m_invaders.size(); ++inv) {
            if (m_invaders[inv].isAlive() && m_bullets[b].getX() == m_invaders[inv].getX() && m_bullets[b].getY() == m_invaders[inv].getY()) {
                m_invaders[inv].destroy();
                m_bullets[b].deactivate();
                m_score += 100;
                // ド派手な爆発火花を大量動的生成!
                spawnExplosion(m_invaders[inv].getX(), m_invaders[inv].getY());
                break;
            }
        }
    }

    // 4. パーティクルの更新(物理移動と寿命減算)
    for (size_t p = 0; p < m_particles.size(); ++p) {
        m_particles[p].update();
    }

    // =========================================================
    // 【最重要:寿命管理と自動消滅フェーズ】
    // C++標準イディオム「erase-remove」により、死んだ実体を完全解放!
    // =========================================================
    // 寿命が尽きた火花粒子を消去
    m_particles.erase(
        std::remove_if(m_particles.begin(), m_particles.end(), isParticleDead),
        m_particles.end()
    );

    // 画面外に出た弾を消去
    m_bullets.erase(
        std::remove_if(m_bullets.begin(), m_bullets.end(), isBulletInactive),
        m_bullets.end()
    );

    // 撃破された敵を消去
    m_invaders.erase(
        std::remove_if(m_invaders.begin(), m_invaders.end(), isInvaderDead),
        m_invaders.end()
    );

    if (m_invaders.empty()) {
        m_gameClear = true;
        m_isRunning = false;
    }
}

void Game::render() {
    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[m_player.getY()][m_player.getX()] = '_';
    buffer[m_player.getY()][m_player.getX() + 1] = 'A';
    buffer[m_player.getY()][m_player.getX() + 2] = '_';

    // 弾
    for (size_t i = 0; i < m_bullets.size(); ++i) {
        if (m_bullets[i].getY() > 0 && m_bullets[i].getY() < SCREEN_HEIGHT - 1) {
            buffer[m_bullets[i].getY()][m_bullets[i].getX()] = '|';
        }
    }

    // 敵
    for (size_t i = 0; i < m_invaders.size(); ++i) {
        buffer[m_invaders[i].getY()][m_invaders[i].getX()] = 'V';
    }

    // 火花パーティクル(最前面に美しく散乱!)
    for (size_t i = 0; i < m_particles.size(); ++i) {
        int px = m_particles[i].getX();
        int py = m_particles[i].getY();
        if (px > 0 && px < SCREEN_WIDTH - 1 && py > 0 && py < SCREEN_HEIGHT - 1) {
            buffer[py][px] = m_particles[i].getGlyph();
        }
    }

    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: " << m_score 
              << " | PARTICLES: " << m_particles.size() 
              << " | BULLETS: " << m_bullets.size() << "    \n";
}

void Game::run() {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursorInfo;
    GetConsoleCursorInfo(hConsole, &cursorInfo);
    cursorInfo.bVisible = FALSE;
    SetConsoleCursorInfo(hConsole, &cursorInfo);

    while (m_isRunning) {
        processInput();
        update();
        render();
        Sleep(33);
    }

    setCursorPosition(0, SCREEN_HEIGHT + 2);
    if (m_gameClear) {
        std::cout << "=========================================\n";
        std::cout << " ★★★ VICTORY! ALL INVADERS DESTROYED! ★★★\n";
        std::cout << "=========================================\n";
    } else {
        std::cout << "=========================================\n";
        std::cout << "               GAME OVER                 \n";
        std::cout << "=========================================\n";
    }
}
📄 main.cpp
#include "Game.h"

// -------------------------------------------------------------
// 【第3章:オブジェクト指向の到達点】
// main関数は「ゲームを開始する」という1つの責任のみを持つ。
// ゲームループすら Game クラスの中に美しくカプセル化された。
// -------------------------------------------------------------
int main() {
    Game game;
    game.run();
    return 0;
}

3.5 オブジェクトの「命の環(ライフサイクル)」3段階プロセス

オブジェクトが生まれてから死ぬまでのメモリ上の流れを、プログラミングの意図と結びつけて解説します。

🐧 ペンギン先輩
すごーーいっ!!敵を倒した瞬間に星くずが四方にパッと飛び散って、数フレーム後にフワッと消えていきました! しかも、あんなに怖かった free() のメモリ管理を1行も書いていないのに、メモリリークが一切起きないなんて魔法みたいです!
🐻‍❄️ シロクマ先生
魔法ではない、これがC++の真骨頂【RAII】じゃ! ただし、現場のエンジニアとして忘れてはならんぞ。 組込みマイコンや車載システムなど「動的メモリ確保(malloc/new)そのものが許されない環境」では、冒頭で学んだ【静的オブジェクトプール】こそが唯一無二の正解じゃ。 一方で、メモリ制約のないPCやサーバー等で可変長データを扱うなら、この std::vector と RAII が極めて強力な武器になる。環境の制約に応じてこの2つを使い分けられることこそが、本物のプロの証なんじゃよ!
💡 このセクションの重要ポイント
  • 【レガシーの正解】静的オブジェクトプール(Static Object Pool): 組込み・車載(MISRA準拠)・リアルタイムゲームエンジン等において、動的ヒープ確保をゼロ化し、断片化リスク排除とO(1)定数時間アロケーションを保証する古典的王道設計。
  • 【参考資料:モダンC++】std::vector と RAII: メモリ制約の緩やかな汎用PC・サーバー環境において、要素数を自動伸縮させつつ、コンストラクタとデストラクタで手動 free() を完全撲滅する現代的設計。
  • Gameクラスによるゲームループの隠蔽: ゲームループすらクラスにカプセル化することで、main関数は「ゲームを開始する」という1つの責任のみを持つ美しい姿になりました。

📝 理解度チェッククイズ

Q1. 組込み機器やリアルタイムゲームエンジンにおいて、動的メモリ確保(malloc/new/std::vector)をあえて使わず「静的オブジェクトプール」が推奨される最大の理由は何でしょう?
  • 1. C++コンパイラが壊れてしまうから
  • 2. メモリ断片化(Heap Fragmentation)による不意の確保失敗を防ぎ、常に一定の処理時間(O(1)確定性)で安全に再利用できるから
  • 3. 静的配列にすると画面の解像度が2倍になるから
  • 4. プログラムのバイナリサイズが100MB小さくなるから
正解と解説を見る
正解:メモリ断片化(Heap Fragmentation)による不意の確保失敗を防ぎ、常に一定の処理時間(O(1)確定性)で安全に再利用できるから
正解です!長期間連続稼働する組込みシステムやリアルタイム制御では、ヒープの確保・解放を繰り返すことでメモリが虫食い状態(断片化)になり、空き容量はあるのに確保に失敗する致命的なバグが起きます。事前に最大数を静的確保して使い回す「静的オブジェクトプール」は、この問題を根絶するレガシー環境の黄金律です。(※なお、メモリ制約のないPC環境では std::vector と RAII が標準的に使われます)
← 前の章へ進む
次の章へ進む →

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

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

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