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
はメモリ確保とコンストラクタ呼び出しを一体化します。
以下は単一オブジェクトの確保・利用・解放の例です。
#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
でクリアするのが基本です。
#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
を使うのが堅実です。
まずは危険な例です。
#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で安全にします。
#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
で配列を解放すると未定義動作です。
特にデストラクタがある型で致命的になります。
#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以降は「トリビアル」「標準レイアウト」などの粒度で語ります)のような「トリビアルな型」では、実質的にコンストラクタ/デストラクタが空になります。
一方、非トリビアルな型では各要素ごとに確実にコンストラクタとデストラクタが呼ばれます。
#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/delete | malloc/free |
---|---|---|
生成対象 | オブジェクト | 生メモリ |
初期化 | コンストラクタ自動呼び出し | なし(手動) |
解放時 | デストラクタ呼び出し | なし |
エラー通知 | 例外(bad_alloc) | nullptr |
配列サポート | new[]/delete[] | サイズ手動管理 |
型安全 | 高い | 低い(キャスト必須) |
例外(std::bad_alloc)とnothrow newの使いどころ
new
は失敗時にstd::bad_alloc
を投げます。
コード全体を例外方針で設計している場合に自然です。
例外を使いたくない環境ではnothrow
版を使ってnullptr
チェックを行います。
#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で安全にラップできます。
#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で防ぎます。
危険例と対策のイメージ:
// 危険: ダングリングの例(実行しないこと)
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::vector
やstd::unique_ptr<T[]>
に任せると安全です。
アラインメントと未初期化アクセスの問題
new
は対象型に必要なアラインメントを満たすメモリを返します。
C++17以降は過剰アラインメント型(alignas
でstd::max_align_t
を超える)にも対応する「アラインドnew」が言語仕様に統合されました。
未初期化のまま読み出すと未定義動作なので、必ず初期化を行います。
#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
が推奨です。
#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>
を優先します。
#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
が必要な場合でも、関数内部に閉じ込めて所有権を明確に返します。
外部に生ポインタを露出しない設計が有効です。
#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
を使い、ペアの混用を避けること、所有権の明確化と例外安全を徹底することが、堅牢なメモリ管理への近道です。