間違えると危険!C++のnew[]とdelete[]で防ぐメモリリーク

C言語からC++にステップアップすると、動的メモリ確保でつまずきやすいのが配列用のnew[]delete[]です。

ここでは両者の正しい対応関係と未定義動作を避けるための具体的な書き方を、初心者の方にもわかりやすい順序で丁寧に解説します。

最後に実践的な運用ポイントもまとめます。

C++のnew[]とdelete[]の基本

newとnew[]の違い

newは単一オブジェクトの生成、new[]は配列(複数個の連続した要素)の生成に使います。

生成される個数と初期化の方法が異なるため、文法だけでなく意味の違いを理解することが重要です。

要点の整理

  • new Tは1個のTを生成します。
  • new T[n]n個のTを連続領域に生成します。
  • オブジェクト型の場合、newは1回、new[]は要素数ぶんコンストラクタが呼ばれます。

以下の表で特徴を並べて確認します。

項目newnew[]
生成個数1n個(連続領域)
典型例int* p = new int(42);int* a = new int[10];
コンストラクタ1回n回
初期化引数可要素ごとにデフォルト/値初期化のみ(引数は個別指定不可)
対応する解放deletedelete[]

初心者の方が特に誤解しやすいのは、new[]で生成した配列に対しては必ずdelete[]が対応するという点です。

次節でさらに深掘りします。

deleteとdelete[]の違い

deleteは単一オブジェクトの破棄、delete[]は配列全体の破棄に使います。

配列の場合は要素ごとにデストラクタが必要なため、ランタイムは配列の個数情報を内部的に保持していることが多いです。

破棄時の挙動

  • delete p; は1回だけデストラクタを呼びます。
  • delete[] a; は要素数回デストラクタを呼びます(一般に逆順)。

この「何回デストラクタを呼ぶか」を実現するため、new[]delete[]はペアで設計されています。

したがって混在させると未定義動作になります。

new[]とdelete[]は対応させる

対応関係は次の通りです。

これ以外の組み合わせは未定義動作です。

確保正しい解放備考
p = new T;delete p;単一オブジェクト
a = new T[n];delete[] a;配列オブジェクト

ここでの「未定義動作」とは、クラッシュする場合もあれば表面上は動いて見える場合もあるということです。

テストでたまたま動いても安全ではありません。

混在は未定義動作になる

new[]で確保したものをdeleteで解放したり、newで確保したものをdelete[]で解放するのは未定義動作です。

内部実装では配列の要素数やアラインメント情報を埋め込むことがあり、それを誤ったdelete系で処理すると破綻します。

悪い例(絶対にしないこと)

C++
// NG例: new[] と delete の混在は未定義動作
int main() {
    int* a = new int[10];
    // ... 何らかの処理 ...
    delete a; // ← 必ず delete[] にしなければならない
    return 0;
}

なお、new/deletemalloc/freeを混在させることも未定義動作です。

この解説ではC++のnew[]/delete[]に絞りますが、Cの関数と混在させないことも強く意識してください。

コンストラクタとデストラクタの挙動

配列のnew[]では、各要素に対してコンストラクタが順に呼ばれ、delete[]では各要素のデストラクタが逆順に呼ばれます。

動作を見える化するためのサンプルを示します。

C++
#include <iostream>

class Tracer {
public:
    Tracer() : id_(next_id_++) {
        std::cout << "Ctor: id=" << id_ << "\n";
    }
    ~Tracer() {
        std::cout << "Dtor: id=" << id_ << "\n";
    }
private:
    int id_;
    static int next_id_;
};

int Tracer::next_id_ = 0;

int main() {
    std::cout << "Allocate array\n";
    Tracer* arr = new Tracer[3]; // デフォルトコンストラクタが3回呼ばれる

    std::cout << "Delete array\n";
    delete[] arr; // デストラクタが逆順(2,1,0)で3回呼ばれる
    return 0;
}
実行結果
Allocate array
Ctor: id=0
Ctor: id=1
Ctor: id=2
Delete array
Dtor: id=2
Dtor: id=1
Dtor: id=0

配列の動的確保と解放のコード例

int配列をnew[]で確保しdelete[]で解放

整数配列は最も基本的な例です。

確保から利用、解放までを一気通貫で示します。

C++
#include <iostream>

int main() {
    const int n = 5;                      // 要素数
    int* a = new int[n];                  // new[] で確保

    // 配列に値を入れる
    for (int i = 0; i < n; ++i) {
        a[i] = i + 1;                     // 1,2,3,4,5
    }

    // 合計を計算
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        sum += a[i];
    }

    std::cout << "sum = " << sum << "\n";

    // 忘れずに解放。配列は delete[] を使う
    delete[] a;
    a = nullptr;                          // ダングリング防止のために明示的に無効化
    return 0;
}
実行結果
sum = 15

クラス配列の生成と破棄の手順

ユーザ定義型の配列でも、基本は同じです。

ただし各要素のコンストラクタとデストラクタが正しく呼ばれることが重要です。

C++
#include <iostream>
#include <string>

class Person {
public:
    Person() : name_("unknown") {
        std::cout << "Ctor: " << name_ << "\n";
    }
    ~Person() {
        std::cout << "Dtor: " << name_ << "\n";
    }
    void set_name(const std::string& n) { name_ = n; }
    const std::string& name() const { return name_; }
private:
    std::string name_;
};

int main() {
    const int n = 3;
    Person* people = new Person[n]; // 3人分のPersonがデフォルト構築される

    // 各要素を設定
    people[0].set_name("Alice");
    people[1].set_name("Bob");
    people[2].set_name("Carol");

    // 利用
    for (int i = 0; i < n; ++i) {
        std::cout << "Hello, " << people[i].name() << "!\n";
    }

    // 破棄。各要素のデストラクタが呼ばれる
    delete[] people;
    people = nullptr;
    return 0;
}
実行結果
Ctor: unknown
Ctor: unknown
Ctor: unknown
Hello, Alice!
Hello, Bob!
Hello, Carol!
Dtor: Carol
Dtor: Bob
Dtor: Alice

二次元配列でのnew[]の注意

C++のnew[]で多次元配列を扱うときは2通りあります。

行ごとに確保する方法と、1塊で確保する方法です。

それぞれ解放手順が異なります。

行ごとに確保する方法(ジャグ配列)

C++
#include <iostream>

int main() {
    const int rows = 2;
    const int cols = 3;

    // 行ポインタの配列を確保
    int** mat = new int*[rows];

    // 各行を個別に確保(行ごとに delete[] が必要)
    for (int r = 0; r < rows; ++r) {
        mat[r] = new int[cols];
    }

    // 値を設定と表示
    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            mat[r][c] = r * 10 + c;
            std::cout << mat[r][c] << (c + 1 == cols ? '\n' : ' ');
        }
    }

    // 解放は「各行をdelete[]」→「行ポインタ配列をdelete[]」の順
    for (int r = 0; r < rows; ++r) {
        delete[] mat[r];
    }
    delete[] mat;
    mat = nullptr;
    return 0;
}
実行結果
0 1 2
10 11 12

この方法は行ごとにサイズが異なる配列も表現できますが、解放の手順を間違えやすい点に注意します。

1塊で確保する方法(連続領域)

C++
#include <iostream>

int main() {
    const int rows = 2;
    const int cols = 3;

    // 要素数 rows*cols の1次元配列として連続確保
    int* mat = new int[rows * cols];

    auto idx = [cols](int r, int c) { return r * cols + c; };

    for (int r = 0; r < rows; ++r) {
        for (int c = 0; c < cols; ++c) {
            mat[idx(r, c)] = r * 10 + c;
            std::cout << mat[idx(r, c)] << (c + 1 == cols ? '\n' : ' ');
        }
    }

    // 解放は1回だけ
    delete[] mat;
    mat = nullptr;
    return 0;
}
実行結果
0 1 2
10 11 12

この方法はキャッシュ効率が良く、解放も1回で済みます。

多くの場合はこちらが扱いやすいです。

よくある間違いとメモリリーク

new[]をdeleteで解放してしまう

配列にdeleteを呼ぶと、要素数分のデストラクタが呼ばれないため未定義動作です。

POD型(intなど)でも、ランタイムが内部ヘッダを参照する実装ではクラッシュやヒープ破壊を招きます。

C++
// NG: new[] に delete は対応しない
int* a = new int[10];
// ...
delete a; // 必ず delete[] a; にする

newをdelete[]で解放してしまう

単一オブジェクトにdelete[]を呼ぶのも未定義動作です。

実装依存のヘッダを誤って読み書きし、ヒープ状態が壊れる可能性があります。

C++
// NG: new に delete[] は対応しない
int* p = new int(42);
// ...
delete[] p; // 必ず delete p; にする

delete[]を忘れてリークになる

確保した配列を解放し忘れるとメモリリークになります。

早期returnや例外で解放コードがスキップされるパターンが典型です。

C++
#include <string>

// NG例: 途中で return して delete[] が実行されない
char* read_line_or_null(bool ok) {
    char* buf = new char[1024];      // new[]
    if (!ok) {
        return nullptr;              // ← ここでリーク
    }
    // ... bufに読み込む処理 ...
    // 正常経路では解放していないのも問題
    return buf;                      // 呼び手が必ず delete[] する契約が必要
}

このような問題を避けるため、C++では所有権を表すスマートポインタを推奨します。

配列にはstd::unique_ptr<T[]>が使えます。

確保したスコープを抜けると自動でdelete[]が呼ばれます。

C++
#include <memory>
#include <iostream>

int main() {
    std::unique_ptr<int[]> a(new int[5]); // 例外や早期returnでも自動解放
    for (int i = 0; i < 5; ++i) a[i] = i;
    std::cout << a[4] << "\n";
    // スコープ末尾で自動的に delete[] が呼ばれる
    return 0;
}
実行結果
4

ただし本記事のテーマはnew[]/delete[]の対応関係です。

実務では配列はまずstd::vectorstd::arrayの利用を検討してください。

メモリリークを防ぐ運用ポイント

確保と解放を一対で書く

new[]を書いた直後に、その解放位置と方法を決めておくと漏れにくくなります。

最低でも同じ関数内の対称位置にdelete[]を書く、あるいはgotobreakなどの制御フローでスキップされないようブロック構造を工夫します。

もっとも効果的なのは、所有権をオブジェクトに閉じ込めるRAII(リソースの獲得は初期化)の活用です。

C++
#include <memory>

void process() {
    // RAIIで自動解放。配列は unique_ptr<T[]> を使う
    std::unique_ptr<char[]> buf(new char[1024]);
    // ... bufを使った処理 ...
} // スコープを抜けると自動で delete[] buf

関数の早期returnに注意する

早期returnのパスごとにdelete[]が確実に実行されるかを点検します。

手動で管理する場合は「1出口方式」にするか、スコープ終了時に必ず解放される仕組みを使います。

C++
// 手動管理の例: 失敗時でも必ず解放
int do_work_manual(bool ok) {
    int* data = new int[100];
    int result = -1;
    if (!ok) {
        result = 0;
        goto cleanup;                 // 例外がない環境での単純な対処
    }
    // ... 処理 ...
    result = 1;
cleanup:
    delete[] data;
    return result;
}

実務では手動管理より、std::unique_ptr<T[]>std::vector<T>の採用が堅実です。

ポインタの所有範囲を明確にする

「誰が解放するのか」をあらかじめ決め、コードやコメントで明記します。

生ポインタを関数に渡すときは、所有権の移動があるのか、借用なのかを区別してください。

所有権を移動するならスマートポインタで表現し、借用ならconst T*またはT*と長さをペアで渡し、呼び出し側のライフタイム内でのみ使用するルールを守ります。

目的推奨の受け渡し方解放の責任
借用(所有権なし)T* ptr, std::size_t size呼び出し元
所有権の移動std::unique_ptr<T[]>受け取り側(スコープ終端で自動解放)
共有所有std::shared_ptr<T>(配列は非推奨)参照カウントが0で自動解放

まとめ

new[]で確保した配列は必ずdelete[]で解放し、newで確保した単一オブジェクトはdeleteで解放する、この1対1の対応関係が最重要ポイントです。

混在させると未定義動作になり、テストでは見逃されがちな潜在バグやメモリ破壊、クラッシュを引き起こします。

配列では要素数分のコンストラクタとデストラクタが呼ばれること、二次元配列の確保と解放順序の違いなども合わせて理解しておくと安全です。

実務では漏れやすい場面(早期return、例外)を避けるために、可能ならstd::vectorstd::unique_ptr<T[]>などRAIIを活用し、所有権の所在を明確に保つ運用を徹底してください。

これらの基本を守ることで、C++におけるメモリリークや未定義動作のリスクを大きく減らすことができます。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!