C++の動的メモリ管理:new/deleteの危険性と現代のスマートポインタ

C++において、変数がメモリ上に存在する期間(寿命)は、主に3つの「記憶域期間」によって決まります。

  1. 自動記憶域期間 (Automatic Storage Duration): 関数やブロック {} の内部で宣言されたローカル変数。そのブロックに入ったときに生成され、ブロックを抜けると自動的に破棄されます。
  2. 静的記憶域期間 (Static Storage Duration): グローバル変数や static キーワード付きの変数。プログラムの開始時に生成され、プログラムの終了時まで存在し続けます。
  3. 動的記憶域期間 (Dynamic Storage Duration): プログラマが new 演算子を使って、任意のタイミングで生成し、delete 演算子を使って任意のタイミングで破棄するオブジェクト。

この記事では、3つ目の「動的記憶域期間」の伝統的な使い方と、それが持つ重大な危険性、そして現代のC++における安全な解決策について解説します。

目次

newdelete:伝統的な動的生成(非推奨)

C++では、new 演算子を使って「ヒープ(Heap)」と呼ばれる広大な空きメモリ領域から、オブジェクトのための領域を動的に確保できます。

new は、確保したメモリ領域へのポインタを返します。

#include <iostream>

int main() {
    // int型オブジェクト1個分のメモリをヒープに動的に確保し、
    // そのアドレスをポインタ 'ptr' に格納する
    int* ptr = new int;

    *ptr = 150; // ポインタ経由で、確保した領域に値を書き込む
    
    std::cout << "ヒープ上の値: " << *ptr << "\n";

    // --- 重要な責任 ---
    // 'new' で確保したメモリは、必ず 'delete' で手動解放しなければならない
    delete ptr;
}

new で確保されたオブジェクトの寿命は、delete が呼ばれるまでです。もし delete を呼び忘れると、そのメモリ領域はプログラムが終了するまで確保されたままとなり、使用可能なメモリが減少していきます。これをメモリリークと呼びます。

配列の動的生成:new[]delete[]

new 演算子は、実行時にサイズを指定して配列を動的に確保するためにも使われます。

#include <iostream>

int main() {
    size_t size;
    std::cout << "必要なバッファのサイズを入力: ";
    std::cin >> size;

    // 'size' 個の double 型要素を持つ配列を動的に確保
    double* buffer = new double[size];

    // 配列として使用
    buffer[0] = 9.9;
    
    // --- 非常に重要なルール ---
    // 'new[]' (配列) で確保したメモリは、
    // 必ず 'delete[]' (配列用delete) で解放しなければならない
    delete[] buffer;
    
    return 0;
}

deletedelete[] は異なる演算子です。new[] で確保したものを delete で解放したり、new で確保したものを delete[] で解放したりすると、未定義動作(プログラムのクラッシュなど)を引き起こします。

new/delete(生のポインタ)が引き起こす問題

newdelete を手動で管理する「生のポインタ」の扱いは、C++におけるバグの最大の原因の一つです。

  1. メモリリーク: delete または delete[] の呼び忘れ。特に関数が途中で return したり、例外がスローされたりすると、delete 文が実行されずにリークが確定します。
  2. ダングリングポインタ: delete でメモリを解放した後も、そのポインタ変数を使い続けること。すでに解放された(あるいは別の何かに再利用されている)メモリ領域を読み書きするため、深刻なエラーを引き起こします。
  3. 二重解放: 同じメモリ領域を誤って二度 delete すること。

これらの問題はすべて、プログラマが「delete を正確に1回だけ呼び出す」という管理責任を負わなければならないことに起因します。

現代C++の解決策 (1):std::vector

new T[size](配列の動的確保)のほぼ全てのユースケースは、std::vector で置き換えるべきです。

<vector> ヘッダが提供する std::vector は、自身の要素のためのメモリを動的に確保・管理するコンテナです。vector オブジェクトがスコープを抜けると(自動記憶域期間)、そのデストラクタが自動的に確保したメモリを解放(delete[])してくれます。

#include <iostream>
#include <vector> // vector を使うために必要

int main() {
    size_t size;
    std::cout << "必要なバッファのサイズを入力: ";
    std::cin >> size;

    // 'size' 個の double 型要素を持つ vector を作成
    // これだけで動的にメモリが確保される
    std::vector<double> buffer(size);

    // 配列と同じように安全に使える
    buffer[0] = 9.9;
    std::cout << "buffer[0] = " << buffer[0] << "\n";
    
    // 'delete[]' は一切不要!
    // main 関数が終了する際、'buffer' 変数が破棄されると
    // メモリは自動的に解放される。

    return 0;
}

現代C++の解決策 (2):スマートポインタ

「配列ではない、単一のオブジェクト」を動的に確保する必要がある場合(例:ポリモーフィズム)は、<memory> ヘッダが提供するスマートポインタを使用します。

スマートポインタは、生のポインタ(int* など)をラップし、ポインタが不要になった時点で(スコープを抜けた時点で)自動的に delete を呼び出してくれるクラスです。

最も基本的で安全なスマートポインタが std::unique_ptr です。

#include <iostream>
#include <memory> // スマートポインタのために必要

class MyObject {
public:
    MyObject(int id) : id_(id) { std::cout << "MyObject(" << id_ << ") 生成\n"; }
    ~MyObject() { std::cout << "MyObject(" << id_ << ") 破棄\n"; }
    void Print() { std::cout << "ID: " << id_ << "\n"; }
private:
    int id_;
};

int main() {
    // 'new' を直接使わず、std::make_unique (C++14以降) を推奨
    // MyObject(101) をヒープに生成し、その管理を ptr が行う
    std::unique_ptr<MyObject> ptr = std::make_unique<MyObject>(101);

    // スマートポインタは、生のポインタのように '*' や '->' でアクセスできる
    ptr->Print();
    
    // 'delete' は一切不要!
    // main 関数が終了する際、'ptr' が破棄されると
    // 管理下の MyObject が自動的に delete される
    
    return 0;
}

実行結果:

MyObject(101) 生成
ID: 101
MyObject(101) 破棄

delete を明示的に呼んでいないにもかかわらず、MyObject が正しく破棄されていることがわかります。この「オブジェクトの寿命がスコープに紐づく」仕組みを RAII (Resource Acquisition Is Initialization) と呼び、現代C++の根幹をなす原則です。

(参考)new と例外処理

new 演算子は、メモリの確保(ヒープ領域の取得)に失敗すると、std::bad_alloc という例外をスローします。

#include <iostream>
#include <new> // std::bad_alloc のために必要

int main() {
    try {
        // 非常に巨大な(おそらく失敗する)メモリを要求
        // size_t の最大値 / 2
        size_t insaneSize = std::numeric_limits<size_t>::max() / 2;
        int* arr = new int[insaneSize];
        
        // 成功した場合(まずないが)
        delete[] arr;
    }
    catch (const std::bad_alloc& e) {
        // new が失敗すると、ここが実行される
        std::cout << "メモリの動的確保に失敗しました: " << e.what() << "\n";
    }
    return 0;
}

古いコードでは new(nothrow) という、例外をスローせずに NULL(または nullptr)を返す形式が使われることもありましたが、現代のC++では例外によるエラーハンドリングが基本です。

(参考)nullptr について

C言語由来の NULL マクロ(実体は 0)は、ポインタが何も指していないことを示すために使われてきました。

C++11以降は、NULL の代わりに、型安全なキーワードである nullptr を使用することが強く推奨されます。nullptr はポインタ型専用の値であり、整数型 0 との曖昧さを排除します。

int* p1 = nullptr; // 現代 C++ (推奨)
// int* p2 = NULL; // C言語スタイル (非推奨)

まとめ(総括)

  • C++の動的メモリ確保(new/delete)は、プログラマにオブジェクトの寿命の完全なコントロールを与えますが、メモリリークなどの重大なバグの温床です。
  • 伝統的な new/delete の手動管理は、現代のC++では可能な限り避けるべきです。

推奨される現代的なアプローチ:

  1. 動的な配列が必要な場合: std::vector を使用する。
  2. 単一のオブジェクトを動的に確保する必要がある場合: std::unique_ptr(または std::shared_ptr)を使用する。
  3. ポインタが何も指さないことを示す場合: nullptr を使用する。

これらのRAII原則に基づいたツール(vectorunique_ptr)を使用することで、C++プログラマは delete の呼び忘れを心配することなく、安全で堅牢なコードを記述できます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次