閉じる

C++のメモリ確保と解放(new, delete) 基本と安全な書き方

C++での動的メモリ確保は、プログラムの柔軟性を高める一方でミスが起きやすい箇所でもあります。

本稿ではnewdeleteを中心に、安全に確保と解放を行うための基本と作法を、初心者向けに順を追って解説します。

配列版のnew[]delete[]の対応や、二重解放・ミスマッチなどの落とし穴にも丁寧に触れます。

C++の動的メモリの基礎(new, delete)

動的メモリとは

動的メモリとは、newで実行時に必要な分だけ確保し、deleteで任意のタイミングで解放する領域のことです。

必要な大きさが実行時まで分からないデータを扱うときに有効です。

静的領域や自動変数(ローカル変数)とは寿命や管理方法が異なります。

いつ使うか

入力サイズに応じて配列の大きさが変わる、GUIのウィジェットを動的に生成する、などの場面で使われます。

固定サイズで済むならローカル変数(スタック)を優先し、必要なときだけ動的メモリを検討します。

スタックとヒープの違い(かんたん解説)

スタックは関数呼び出しに伴って自動で確保・解放される領域で、高速ですがサイズや寿命がスコープに縛られます。

ヒープはnewdeleteで手動管理する領域で、柔軟ですがミスが起きやすいです。

速度・安全のスタック、柔軟性のヒープと覚えると良いでしょう。

以下は対比の表です。

観点スタックヒープ
確保/解放自動手動(new/delete)
速度速い相対的に遅い
寿命スコープ終了までdeleteするまで
使いどころ固定サイズ、短寿命可変サイズ、長寿命

ポインタとnullptrの基本

newはメモリアドレスを返すので、変数にはポインタ型を使います。

未初期化ポインタは危険なので、必ずnullptrで初期化しましょう。

nullptrは「どこも指していない」ことを意味します。

例: ポインタの初期化

C++
#include <iostream>

int main() {
    int* p = nullptr;            // どこも指していないことを明示
    if (!p) {
        std::cout << "p は nullptr です\n";
    }
    // 後で new して使う準備ができている
}
実行結果
p は nullptr です

メモリの所有と寿命の考え方

「誰が解放するのか(所有者)を1つに決める」ことが重要です。

所有者が曖昧だと二重解放やメモリリークにつながります。

所有者はdeleteの責任を持ち、不要になったタイミングで解放します。

複数のポインタが同じメモリを指す場合は、所有権のあるポインタと参照用の非所有ポインタを区別しましょう。

newの使い方(単体と配列)

単一オブジェクトをnewで確保

new Tは型Tのオブジェクトを1つ確保し、そのアドレスを返します。

スカラー型(int, doubleなど)はデフォルトでは未初期化です。

未初期化値の読み取りは未定義動作のため、割り当ててから使います。

C++
#include <iostream>

int main() {
    int* p = new int;     // 未初期化(値は不定)。直ちに読み取らないこと
    *p = 10;              // まず安全に値を設定
    std::cout << "*p = " << *p << '\n';

    delete p;             // 忘れずに解放
    p = nullptr;          // ダングリング防止
}
実行結果
*p = 10

必ず初期化する(例: new T())

未初期化はバグの温床です。

確保と同時に初期化しましょう。

丸括弧new T()や波括弧new T{...}が使えます。

C++
#include <iostream>

int main() {
    int* a = new int();      // 値初期化 → 0 になる
    int* b = new int{42};    // 直接初期化 → 42 になる

    std::cout << "*a = " << *a << ", *b = " << *b << '\n';

    delete a;
    delete b;
}
実行結果
*a = 0, *b = 42

初期化の要点

new T()はスカラー型を0初期化、new T{v}は指定値で初期化します。

クラス型ではコンストラクタが選ばれます。

クラスはコンストラクタが呼ばれる

クラス(構造体)をnewすると、対応するコンストラクタが自動で呼ばれます。

生成と同時に正しい状態へ初期化できるのがC++の利点です。

C++
#include <iostream>

struct Point {
    int x, y;
    Point(int x, int y) : x(x), y(y) {
        std::cout << "Point(" << x << ", " << y << ") constructed\n";
    }
    ~Point() {
        std::cout << "Point(" << x << ", " << y << ") destroyed\n";
    }
};

int main() {
    Point* p = new Point(3, 4);  // コンストラクタが呼ばれる
    std::cout << "p->x = " << p->x << ", p->y = " << p->y << '\n';
    delete p;                    // デストラクタが呼ばれる
}
実行結果
Point(3, 4) constructed
p->x = 3, p->y = 4
Point(3, 4) destroyed

配列の確保はnew[]を使う

複数要素を確保する場合はnew T[n]を使います。

解放はdelete[]で対応させます。

スカラー型配列はnew intnnew int[n]{}でゼロ初期化できます。

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

int main() {
    // ゼロ初期化された int 配列
    int* a = new int[5]{};  // すべて 0
    for (int i = 0; i < 5; ++i) std::cout << a[i] << ' ';
    std::cout << '\n';

    // クラス型配列は各要素のデフォルトコンストラクタが呼ばれる
    std::string* s = new std::string[3]; // "" が3つ
    s[0] = "C++";
    s[1] = "new";
    s[2] = "delete";
    for (int i = 0; i < 3; ++i) std::cout << s[i] << ' ';
    std::cout << '\n';

    delete[] a;  // 配列は delete[] で解放
    delete[] s;
}
実行結果
0 0 0 0 0 
C++ new delete

deleteの使い方と安全な書き方

単一オブジェクトをdeleteで解放

newで確保したら、対応してdeleteで解放します。

解放後はnullptrを代入してダングリングを防ぎます。

C++
#include <iostream>

int main() {
    double* p = new double{3.14};
    std::cout << "*p = " << *p << '\n';
    delete p;    // メモリ解放
    p = nullptr; // 解放済みを明示
    std::cout << "解放後 p は " << (p == nullptr ? "nullptr" : "有効") << '\n';
}
実行結果
*p = 3.14
解放後 p は nullptr

配列はdelete[]で解放(new[]と対応)

配列はdelete[]で解放します。

クラス型配列では各要素のデストラクタが順番に呼ばれます。

C++
#include <iostream>

int main() {
    int* a = new int[3]{1, 2, 3};
    delete[] a;   // 忘れずに [] を付ける
}

deleteとdelete[]のミスマッチに注意

単一オブジェクトにdelete[]、配列にdeleteを使うのは未定義動作です。

new と delete、new[] と delete[] を必ず対応させます。

C++
// これはダメな例(実行しないでください)
// int* p = new int{42};
// delete[] p;     // × 未定義動作

// int* a = new int[3]{1,2,3};
// delete a;       // × 未定義動作

delete後はnullptrを代入(二重解放を防ぐ)

解放直後にptr = nullptr;を徹底すると、誤って再度deleteしてしまう二重解放を抑止できます。

C++
#include <iostream>

int main() {
    int* p = new int{100};
    delete p;
    p = nullptr;        // 重要な一行
    delete p;           // nullptr への delete は安全(何も起こらない)
    std::cout << "OK\n";
}
実行結果
OK

delete nullptrは安全(何も起きない)

delete nullptr;delete[] nullptr;は何も起きません

防御的にnullptrを代入する習慣は有効です。

初心者向けチェックリストと落とし穴

newしたら必ず対応するdeleteを書く

確保と解放は必ず対にするのが基本です。

関数内でnewしたら、同じ関数内のすべてのパスでdeleteを呼ぶようにします。

例: 関数内で必ず解放する

C++
#include <iostream>

bool process(bool fail) {
    int* buf = new int[4]{};  // new[]
    if (fail) {
        delete[] buf;         // 早期リターン前に解放
        return false;
    }
    // ... 正常処理 ...
    delete[] buf;             // 通常パスでも解放
    return true;
}

int main() {
    std::cout << (process(false) ? "ok" : "ng") << '\n';
    std::cout << (process(true)  ? "ok" : "ng") << '\n';
}
実行結果
ok
ng

所有者は1つに決める(ポインタの共有に注意)

複数のポインタが同じ領域を「所有」すると、二重解放か解放漏れのどちらかに陥りがちです。

所有者は1つ、他は借用(非所有)と決め、コメントや変数名で明示します。

C++
// 所有者: owner、借用: view
int* owner = new int{5};
int* view  = owner;  // 借用: 解放しない約束

// ... view は読み取り専用として使う ...

delete owner;  // 解放は所有者のみ
owner = nullptr;
view  = nullptr; // 以降は使わない

早期returnでもdeleteを忘れない

条件分岐で早期にreturnすると解放を忘れがちです。

エラー分岐の直前で解放するか、最後に集約して解放しましょう。

C++
#include <iostream>

bool initAndRun(bool bad1, bool bad2) {
    int* p = new int{1};

    if (bad1) {        // 早期失敗
        delete p;      // 忘れずに解放
        return false;
    }

    if (bad2) {        // もう一つの早期失敗
        delete p;      // ここも解放
        return false;
    }

    // 正常系
    std::cout << "run\n";
    delete p;          // 最後に解放
    return true;
}
実行結果
run

delete後に使わない(ダングリングを防ぐ)

解放したポインタを使うと未定義動作です。

解放直後にnullptrを代入し、その後の使用を検出できるようにします。

C++
#include <iostream>

int main() {
    int* p = new int{7};
    delete p;
    p = nullptr;           // ダングリング防止

    if (!p) std::cout << "p は無効です\n";
}
実行結果
p は無効です

よくあるエラーのパターンと対処法

典型的な失敗と修正の仕方を整理します。

パターン症状/原因対処
new[] を delete で解放未定義動作、クラッシュdelete[]を使う
delete の書き忘れメモリリークすべての分岐に解放を配置、設計時に所有者を明確化
未初期化ポインタを使用未定義動作まずnullptrで初期化、new T()等で値初期化
解放後の利用ダングリング参照解放直後にnullptrを代入し以後使わない
複数所有二重解放/解放漏れ所有者は1つ、他は借用とする

現代C++では本稿の範囲を超えますが、自動解放を行うスマートポインタを用いると、ここでの多くの問題を回避できます。

とはいえ、new/delete の正しい対応関係と初期化の徹底は基礎として必須です。

まとめ

new/delete は「確保と解放を必ず対にする」「所有者は1つ」「初期化を徹底する」の3点を守ることで安全に使えます。

配列はnew[]delete[]を対応させ、解放後はnullptrを代入してダングリングを防ぎます。

未初期化やミスマッチは未定義動作に直結するため、サンプルのパターンを繰り返し実践して体に覚えさせてください。

現代的な手法(スマートポインタ)を使う前提でも、raw な new/delete の正しい理解がコードの健全性を支えます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

C++をこれから学ぶ方に向けて、基礎的な文法や標準ライブラリの使い方を紹介します。モダンな書き方も初心者に合わせてやさしく説明しています。

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

URLをコピーしました!