C++のメモリ管理入門:malloc/freeの代わりにnew/deleteを使う方法【違い・使い分け・注意点】

C++での動的メモリ管理は、プログラムの性能や堅牢性に直結します。

本記事では、C言語のmalloc/freeではなく、C++のnew/deleteを使って安全かつ明確にメモリを確保・解放する方法を、違いや使い分け、注意点を交えながら丁寧に解説します。

RAIIやスマートポインタまで踏み込み、実用的なベストプラクティスを示します。

C++のメモリ管理の基礎:malloc/freeとnew/deleteの位置づけ

C++では、スタック領域とヒープ領域があり、動的な寿命を持つオブジェクトはヒープに確保します。

C言語のmalloc/freeはバイト単位の生メモリを扱う関数で、型やコンストラクタ/デストラクタの概念がありません。

これに対してC++のnew/deleteは「オブジェクト」を生成・破棄する演算子で、次の特徴があるため、C++の基本選択肢はnew/deleteです。

  • 型に基づく生成と初期化(コンストラクタが呼ばれる)
  • 破棄時にデストラクタが呼ばれる
  • 例外(std::bad_alloc)で失敗を通知する
  • 配列用のnew[]/delete[]がある

malloc/freeは、Cとの相互運用や特殊な割り付け戦略が必要な場合などの例外的な場面で使い分けます。

new/deleteの基本:単一オブジェクトの確保と解放

newの構文と初期化(コンストラクタ呼び出し)

newはメモリ確保とコンストラクタ呼び出しを一体化します。

以下は単一オブジェクトの確保・利用・解放の例です。

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

struct Point {
    int x;
    // コンストラクタで初期化
    explicit Point(int v) : x(v) {
        std::cout << "Point ctor: x=" << x << "\n";
    }
    ~Point() {
        std::cout << "Point dtor: x=" << x << "\n";
    }
};

int main() {
    // newでオブジェクトを生成(コンストラクタが呼ばれる)
    Point* p = new Point(42);

    std::cout << p->x << "\n"; // 利用

    // deleteで解放(デストラクタが呼ばれる)
    delete p;
    p = nullptr; // ダングリング対策としてヌルクリア
}
実行結果
Point ctor: x=42
42
Point dtor: x=42

deleteの正しいタイミングと未定義動作の回避

deleteは、対応するnewで生成したオブジェクトに対して一度だけ呼びます。

二度呼ぶと未定義動作です。

早すぎる解放はダングリングポインタを生み、遅すぎる解放はメモリリークを招きます。

利用が終わった直後にdeleteし、ポインタをnullptrでクリアするのが基本です。

C++
#include <iostream>

struct Logger {
    ~Logger(){ std::cout << "Logger destroyed\n"; }
};

int main() {
    Logger* lg = new Logger();

    // ここでlgを使い切ったと判断できるタイミングで解放する
    delete lg;
    lg = nullptr; // 二重解放や誤用の予防

    // if (lg) delete lg; // こうした冗長なガードはnullptr化があれば不要
}
実行結果
Logger destroyed

例外安全とスコープ管理の考え方

例外が飛ぶと、deleteに到達せずリークする恐れがあります。

try-catchで都度ケアするより、RAII(後述)やstd::unique_ptrを使うのが堅実です。

まずは危険な例です。

C++
#include <iostream>
#include <stdexcept>

struct Widget {
    Widget(){ std::cout << "Widget constructed\n"; }
    ~Widget(){ std::cout << "Widget destroyed\n"; }
};

void risky() {
    Widget* w = new Widget();
    // ここで例外が発生すると、deleteに到達しない
    throw std::runtime_error("oops");
    delete w; // 実行されない
}

int main() {
    try {
        risky();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << "\n";
    }
}
実行結果
Widget constructed
caught: oops

RAIIで安全にします。

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

struct Widget {
    Widget(){ std::cout << "Widget constructed\n"; }
    ~Widget(){ std::cout << "Widget destroyed\n"; }
};

void safe() {
    auto w = std::make_unique<Widget>(); // スコープを抜ければ自動で破棄
    throw std::runtime_error("oops");
}

int main() {
    try {
        safe();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << "\n";
    }
}
実行結果
Widget constructed
Widget destroyed
caught: oops

new[]/delete[]:配列のメモリ確保と解放の注意点

new[]とdelete[]の組み合わせミスの危険性

配列にはnew[]delete[]のペアを必ず対応させます。

deleteで配列を解放すると未定義動作です。

特にデストラクタがある型で致命的になります。

C++
#include <iostream>

struct Tracer {
    Tracer(){ std::cout << "ctor\n"; }
    ~Tracer(){ std::cout << "dtor\n"; }
};

int main() {
    Tracer* arr = new Tracer[3]; // 配列のコンストラクタが3回呼ばれる

    // 正しい解放
    delete[] arr; // 各要素のデストラクタが3回呼ばれる

    // 誤り例(実行しないこと):
    // Tracer* arr2 = new Tracer[3];
    // delete arr2; // 未定義動作:一部しか破棄されない可能性
}
実行結果
ctor
ctor
ctor
dtor
dtor
dtor

POD/非PODでのコンストラクタ・デストラクタ呼び出しの違い

POD(古い用語。

C++11以降は「トリビアル」「標準レイアウト」などの粒度で語ります)のような「トリビアルな型」では、実質的にコンストラクタ/デストラクタが空になります。

一方、非トリビアルな型では各要素ごとに確実にコンストラクタとデストラクタが呼ばれます。

C++
#include <iostream>
#include <vector>

struct NonTrivial {
    int id;
    NonTrivial(int i=0) : id(i) { std::cout << "NonTrivial ctor: " << id << "\n"; }
    ~NonTrivial(){ std::cout << "NonTrivial dtor: " << id << "\n"; }
};

int main() {
    // トリビアルなint配列
    int* a = new int[3]{1,2,3}; // 値初期化が行われる(初期化リスト使用)
    std::cout << a[0] << "," << a[1] << "," << a[2] << "\n";
    delete[] a;

    // 非トリビアルな型の配列
    NonTrivial* b = new NonTrivial[3]; // デフォルト引数で3回コンストラクト
    delete[] b; // 3回デストラクト
}
実行結果
1,2,3
NonTrivial ctor: 0
NonTrivial ctor: 0
NonTrivial ctor: 0
NonTrivial dtor: 0
NonTrivial dtor: 0
NonTrivial dtor: 0

malloc/freeとの違いと使い分け【型安全・初期化・例外・パフォーマンス】

型安全性と初期化:new/deleteはオブジェクト指向的

newは型に基づいたオブジェクト生成と初期化を一度に行います。

mallocは生メモリを返すだけで、キャストやmemset等の手動初期化が必要です。

C++ではコンストラクタ/デストラクタや例外安全、RAIIと自然に連携するnew/deleteが基本です。

観点new/deletemalloc/free
生成対象オブジェクト生メモリ
初期化コンストラクタ自動呼び出しなし(手動)
解放時デストラクタ呼び出しなし
エラー通知例外(bad_alloc)nullptr
配列サポートnew[]/delete[]サイズ手動管理
型安全高い低い(キャスト必須)

例外(std::bad_alloc)とnothrow newの使いどころ

newは失敗時にstd::bad_allocを投げます。

コード全体を例外方針で設計している場合に自然です。

例外を使いたくない環境ではnothrow版を使ってnullptrチェックを行います。

C++
#include <iostream>
#include <new>
#include <limits>

int main() {
    try {
        // 失敗しやすい極端なサイズで例
        std::size_t huge = std::numeric_limits<std::size_t>::max();
        int* p = new int[huge]; // ほぼ確実にbad_alloc
        delete[] p; // 到達しない想定
    } catch (const std::bad_alloc& e) {
        std::cout << "bad_alloc caught: " << e.what() << "\n";
    }

    // 例外を使わない方針
    int* q = new (std::nothrow) int[10'000'000'000ull]; // 失敗時nullptr
    if (!q) {
        std::cout << "allocation failed, got nullptr\n";
    } else {
        delete[] q;
    }
}
実行結果メモリ確保に失敗しない場合は出力はありません
bad_alloc caught: std::bad_alloc
allocation failed, got nullptr

いつmalloc/freeを使うべきか(Cとのインターフェース等)

CのライブラリやシステムAPIがmallocで確保したバッファを返し、freeで解放を求める場合は、同じペアで扱う必要があります。

その際もC++側ではRAIIで安全にラップできます。

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

// C APIを模した: mallocで確保、freeで解放を要求
void* c_api_get_buffer(std::size_t n) {
    return std::malloc(n);
}

// freeを呼ぶデリータ
struct FreeDeleter {
    void operator()(void* p) const noexcept {
        std::free(p);
        std::cout << "freed by free\n";
    }
};

int main() {
    // unique_ptrでCのバッファを安全に管理
    std::unique_ptr<void, FreeDeleter> buf(c_api_get_buffer(128));
    if (!buf) {
        std::cerr << "allocation failed\n";
        return 1;
    }
    std::cout << "buffer acquired\n";
    // スコープを抜けると自動でfree
}
実行結果
buffer acquired
freed by free

なお、mallocで確保したメモリに対してdeleteを呼んだり、newで確保したメモリにfreeを呼ぶのは未定義動作です。

必ず同じペアを対応させます。

よくある落とし穴と注意点

二重解放・ダングリングポインタ・メモリリーク

  • 二重解放はクラッシュや未定義動作の原因です。delete後にnullptr代入で予防します。
  • ダングリングポインタは、対象が解放されたあともポインタが残っている状態です。所有権を明確にし、スマートポインタを使うことで回避できます。
  • 例外や早期リターンでdeleteに到達しないとリークします。RAIIで防ぎます。

危険例と対策のイメージ:

C++
// 危険: ダングリングの例(実行しないこと)
int* raw = nullptr;
{
    auto up = std::make_unique<int>(10);
    raw = up.get();
} // upが破棄、rawはダングリング
// *raw = 5; // 未定義動作

// 対策: 所有権を共有するならshared_ptrを使う
// auto sp = std::make_shared<int>(10);
// int* alias = sp.get(); // spの寿命中のみ有効

deleteとdelete[]の取り違え

配列にはdelete[]、単一要素にはdeleteです。

型のデストラクタが複数回呼ばれるかどうかに直結します。

混用は未定義動作になります。

配列管理はstd::vectorstd::unique_ptr<T[]>に任せると安全です。

アラインメントと未初期化アクセスの問題

newは対象型に必要なアラインメントを満たすメモリを返します。

C++17以降は過剰アラインメント型(alignasstd::max_align_tを超える)にも対応する「アラインドnew」が言語仕様に統合されました。

未初期化のまま読み出すと未定義動作なので、必ず初期化を行います。

C++
#include <iostream>
#include <cstdint>

struct alignas(64) CacheLine {
    char data[64];
};

int main() {
    CacheLine* p = new CacheLine; // C++17以降は64バイト境界に整列が保証
    std::uintptr_t addr = reinterpret_cast<std::uintptr_t>(p);
    std::cout << "addr % 64 = " << (addr % 64) << "\n";
    delete p;
}
実行結果
addr % 64 = 0

注: 実行環境やビルド設定により表示は異なる可能性があります。

ベストプラクティス:RAIIとスマートポインタで安全に

unique_ptr/shared_ptrでnew/deleteをラップする

手でnew/deleteを書く行数を減らし、例外に安全なコードを心がけます。

単独所有はstd::unique_ptr、共有所有はstd::shared_ptrを使います。

生成はstd::make_unique/std::make_sharedが推奨です。

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

struct Obj {
    Obj(){ std::cout << "Obj()\n"; }
    ~Obj(){ std::cout << "~Obj()\n"; }
};

int main() {
    auto up = std::make_unique<Obj>();  // 例外安全、最小オーバーヘッド
    {
        auto sp = std::make_shared<Obj>(); // 複数箇所で共有可能
        auto sp2 = sp; // 参照カウント+1
        std::cout << "shared owners present\n";
    } // sp, sp2がスコープアウトしても、最後の所有者が消えるまで存続
}
実行結果
Obj()
Obj()
shared owners present
~Obj()
~Obj()

注: 上の出力は生成タイミングの違いを示すため2つのObjを作っています。

実際のライフサイクルは所有者数に依存します。

配列の場合はstd::unique_ptr<T[]>を使うとdelete[]が自動で選ばれます。

より高機能なコンテナが必要ならstd::vector<T>を優先します。

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

int main() {
    std::unique_ptr<int[]> arr(new int[3]{1,2,3}); // または std::make_unique<int[]>(3)
    std::cout << arr[0] << "," << arr[1] << "," << arr[2] << "\n";
} // 自動でdelete[]
実行結果
1,2,3

new/deleteを直接書く場面を最小化(factory/所有権の明確化)

newが必要な場合でも、関数内部に閉じ込めて所有権を明確に返します。

外部に生ポインタを露出しない設計が有効です。

C++
#include <memory>
#include <string>

struct Connection {
    explicit Connection(std::string url) : url_(std::move(url)) {}
    // ...
private:
    std::string url_;
};

// 工場関数:所有権はunique_ptrで返す
std::unique_ptr<Connection> make_connection(std::string url) {
    return std::make_unique<Connection>(std::move(url));
}

int main() {
    auto conn = make_connection("https://example.com");
    // connがスコープを抜ければ自動で解放
}

この設計により、解放忘れや二重解放のリスクを著しく低減できます。

まとめ

C++における動的メモリ管理では、new/deleteは「オブジェクトの生成と破棄」を正しく扱うための基本手段です。

malloc/freeとの違いは、型安全性、初期化、デストラクタ呼び出し、例外によるエラー通知に集約されます。

配列はnew[]/delete[]を対応させ、未初期化アクセスやアラインメントにも配慮が必要です。

とはいえ、現代C++ではRAIIとスマートポインタの利用が最重要のベストプラクティスであり、生のnew/deleteは最小限に留めるのが安全で効率的です。

C API連携など特殊な場面のみmalloc/freeを使い、ペアの混用を避けること、所有権の明確化と例外安全を徹底することが、堅牢なメモリ管理への近道です。

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

URLをコピーしました!