17:デストラクタとオブジェクトの寿命

はじめに

C++では、オブジェクトの生成時にコンストラクタが呼ばれ、破棄されるときにデストラクタが自動的に呼ばれます。
この仕組みは、オブジェクトの「生まれてから消えるまで」の流れを制御するために重要です。


動的メモリ確保とオブジェクトの寿命

C++では new を使うとヒープ領域にメモリを確保し、delete で解放します。
この2つを使うことで、プログラム実行中に「必要なタイミングでオブジェクトを作り、不要になったら消す」ことができます。

// 17_memory_basic.cpp
#include <iostream>
using namespace std;

class Player {
public:
    Player() { cout << "プレイヤー生成!" << endl; }
    ~Player() { cout << "プレイヤー削除!" << endl; }
};

int main() {
    Player* p = new Player(); // 動的に生成
    cout << "プレイ中..." << endl;
    delete p;                 // 明示的に削除(デストラクタが呼ばれる)
    return 0;
}

デストラクタは~Player()のように、~クラス名と同名関数として定義します。 deleteが動作するタイミングで自動的に呼び出されるメソッドとなります。

実行結果(例)

プレイヤー生成!
プレイ中...
プレイヤー削除!

👉 new で確保したオブジェクトは delete で明示的に破棄しないと、メモリが残り続けます。


動的なオブジェクトの生成(new)

  1. 単体オブジェクトを作る
    // 17_new_single.cpp
    Player* p = new Player();        // コンストラクタが呼ばれる
  2. コンストラクタ引数付きで作る
    // 17_new_with_args.cpp
    class Enemy { public: Enemy(int hp) {/*...*/} };
    Enemy* e = new Enemy(100);       // 引数を渡して生成
  3. 配列を作る
    // 17_new_array.cpp
    Enemy* es = new Enemy[10];       // デフォルトコンストラクタ必須
  4. 生成に失敗したら?
    // 17_new_nothrow.cpp
    Enemy* e = new (nothrow) Enemy(100);
    if (!e) { /* メモリ不足時の処理 */ }

動的なオブジェクトの削除(delete)

  1. 単体オブジェクトを消す
    // 17_delete_single.cpp
    delete p;            // デストラクタが呼ばれ、メモリが解放される
    p = nullptr;         // ぶら下がりポインタ対策(推奨)
  2. 配列を消す(重要:delete[] を使う)
    // 17_delete_array.cpp
    delete[] es;         // 配列は必ず delete[]
    es = nullptr;
  3. 二重deleteを防ぐ
  4. delete nullptr; は安全

よくある落とし穴チェックリスト


// 17_memory_basic.cpp
#include <iostream>
using namespace std;

class Player {
public:
    Player() { cout << "プレイヤー生成!" << endl; }
    ~Player() { cout << "プレイヤー削除!" << endl; }
};

int main() {
    Player* p = new Player(); // 動的に生成
    cout << "プレイ中..." << endl;
    delete p;                 // 明示的に削除(デストラクタが呼ばれる)
    return 0;
}

実行結果(例)

プレイヤー生成!
プレイ中...
プレイヤー削除!

👉 new で確保したオブジェクトは delete で明示的に破棄しないと、メモリが残り続けます。


💡ゲーム開発と動的メモリ確保

ゲームの世界では、次々とキャラクターや弾、エフェクトなどが登場します。
これらをすべて最初からメモリ上に確保しておくのは非効率です。

たとえば、

このように、必要なときだけ作り、不要になったら削除するという仕組みが「動的なメモリ確保」の考え方です。

// 17_game_enemy_spawn.cpp
#include <iostream>
#include <vector>
using namespace std;

class Enemy {
public:
    Enemy(int id) { cout << "敵" << id << " 出現!" << endl; }
    ~Enemy() { cout << "敵 消滅!" << endl; }
};

int main() {
    vector<Enemy*> enemies;

    // 敵を3体出現させる
    for (int i = 0; i < 3; i++) {
        enemies.push_back(new Enemy(i + 1));
    }

    cout << "敵を全て倒した!" << endl;

    // 倒した敵を削除
    for (Enemy* e : enemies) {
        delete e;
    }

    return 0;
}

実行結果(例)

敵1 出現!
敵2 出現!
敵3 出現!
敵を全て倒した!
敵 消滅!
敵 消滅!
敵 消滅!

👉 newdelete が連動して動作しており、オブジェクトの寿命管理の大切さが実感できる。



🎮 ミニゲーム:コンソールで敵出現&撃破(new/delete 実践)

new で動的に敵を生成し、倒したら delete で解放する流れを体験する。

// 17_console_mini_game.cpp
#include <iostream>
#include <vector>
#include <limits>
using namespace std;

class Enemy {
    int id;
    int hp;
public:
    Enemy(int id_, int hp_) : id(id_), hp(hp_) {
        cout << "敵" << id << " 出現!(HP=" << hp << ")";
    }
    ~Enemy() {
        cout << "敵" << id << " 消滅!";
    }
    void show(int idx) const {
        cout << idx << ": 敵" << id << " [HP=" << hp << "]";
    }
    void takeDamage(int d) { hp -= d; }
    bool isDead() const { return hp <= 0; }
};

int main() {
    vector<Enemy*> enemies;
    int nextId = 1;
    const int DAMAGE = 15;

    cout << "=== Console Mini Game ===";
    cout << "各ターンで敵が1体出現。番号を選んで攻撃(" << DAMAGE << "ダメージ)。-1で終了。";

    for (int turn = 1; turn <= 8; ++turn) { // 8ターンだけ遊ぶ
        // 1) 敵の出現(動的生成)
        int hp = 20 + (turn * 5 % 15); // 20〜34くらいのHPで変化
        enemies.push_back(new Enemy(nextId++, hp));

        // 2) 状態表示
        cout << "--- Turn " << turn << " ---";
        if (enemies.empty()) cout << "敵はいない";
        for (size_t i = 0; i < enemies.size(); ++i) enemies[i]->show((int)i + 1);

        // 3) 入力受付
        cout << "攻撃する敵の番号を入力(-1で終了、0でスキップ)> ";
        int choice;
        if (!(cin >> choice)) { // 入力失敗対策
            cin.clear();
            cin.ignore(numeric_limits<streamsize>::max(), '');
            cout << "入力エラー。スキップします。";
            choice = 0;
        }
        if (choice == -1) break; // 終了
        if (choice > 0 && (size_t)choice <= enemies.size()) {
            Enemy* target = enemies[choice - 1];
            target->takeDamage(DAMAGE);
            cout << "→ 敵に" << DAMAGE << "ダメージ!";
            if (target->isDead()) {
                cout << "撃破!メモリ解放します。";
                delete target;                         // 4) 撃破 → delete
                enemies.erase(enemies.begin() + (choice - 1)); // ベクタから除去
            }
        } else {
            cout << "スキップ。";
        }
    }

    // 5) ゲーム終了時の後始末(delete忘れ防止)
    for (Enemy* e : enemies) delete e;
    enemies.clear();

    cout << "おつかれさまでした!";
    return 0;
}

ポイント

まずはこの生ポインタ版で寿命管理を体験 → その後に std::unique_ptr などへ進むと理解が深まります。


✅ まとめ