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. 単体オブジェクトを作る

Player* p = new Player();        // コンストラクタが呼ばれる

2. コンストラクタ引数付きで作る

class Enemy { public: Enemy(int hp) {/*...*/} };
Enemy* e = new Enemy(100);       // 引数を渡して生成

3. 配列を作る

Enemy* es = new Enemy[10];       // デフォルトコンストラクタ必須

4. 生成に失敗したら?

充分なメモリがないと std::bad_alloc 例外が投げられます。例外を使わない環境では new (nothrow)nullptr が返ることもあります。

Enemy* e = new (nothrow) Enemy(100);
if (!e) { /* メモリ不足時の処理 */ }

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

1. 単体オブジェクトを消す

delete p;            // デストラクタが呼ばれ、メモリが解放される
p = nullptr;         // ぶら下がりポインタ対策(推奨)

2. 配列を消す(重要:delete[] を使う)

delete[] es;         // 配列は必ず delete[]
es = nullptr;
二重deleteを防ぐポイント
同じポインタに対して delete を二回呼ばないこと。delete 後に nullptr を代入しておくと安全。delete nullptr; は安全な空操作です。

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

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

// 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 出現!
敵を全て倒した!
敵 消滅!
敵 消滅!
敵 消滅!

理解度チェック

問題17-1

次のコードの実行結果はどれですか?

#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 Player() でコンストラクタが呼ばれ "生成"、その後 "使用中" を表示、delete p でデストラクタが呼ばれ "削除" と表示されます。

問題17-2

デストラクタの定義として正しいものはどれですか?(クラス名が Enemy の場合)

解説を表示 正解:イ
デストラクタは ~クラス名() の形で定義します。引数も戻り値も持ちません。オブジェクトが破棄される際に自動的に呼ばれます。

問題17-3

次のコードには問題があります。何が問題ですか?

int main() {
    Enemy* e = new Enemy(1);
    // delete を忘れた
    return 0;
}
解説を表示 正解:ウ
new で確保したメモリは delete しない限り解放されません。デストラクタも呼ばれません。プログラムが短時間で終了する場合はOSが回収しますが、長時間動作するプログラムや大量に生成するゲームでは深刻なメモリリークになります。

問題17-4

次のコードの出力はどれですか?

#include <iostream>
#include <vector>
using namespace std;

class Item {
    int id;
public:
    Item(int i) : id(i) { cout << "Item" << id << " 生成" << endl; }
    ~Item() { cout << "Item" << id << " 削除" << endl; }
};

int main() {
    vector<Item*> items;
    items.push_back(new Item(1));
    items.push_back(new Item(2));
    for (Item* it : items) delete it;
    return 0;
}
解説を表示 正解:ア
push_back の順に Item1 → Item2 が生成されます。その後 range-for で先頭から順に delete するので Item1 → Item2 の順でデストラクタが呼ばれます。

コーディング演習

演習1:基本コードを動かす

17_01_destructor.cpp を作成し、次のコードを入力して実行しましょう。コンストラクタとデストラクタが呼ばれるタイミングを確認してください。
// main.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;

    for (int i = 0; i < 3; i++) {
        enemies.push_back(new Enemy(i + 1));
    }

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

    for (Enemy* e : enemies) {
        delete e;
    }

    return 0;
}

演習2:コードを改造する

17_01_Destructormain.cpp をコピーして 17_02_DestructorMod に貼り付け、次の変更を加えてみましょう:

まとめ