C++でメモリを手動管理するうえで、newとdeleteは避けて通れない基本です。
スタックとヒープの違い、単一オブジェクトの確保と解放、ポインタの安全な扱い、例外発生時の後始末など、初心者がつまずきやすい箇所を丁寧に整理しながら、段階的に解説します。
実行例を交え、deleteのし忘れや二重解放といった典型的なミスの回避策も合わせて学びます。
newとdeleteの基本(C++のメモリの確保と解放)
動的メモリとは?(スタックとヒープ)
プログラムが使うメモリには大きく分けてスタックとヒープがあります。
スタックは関数呼び出しに応じて自動的に確保・解放される一時的な領域で、ローカル変数が置かれます。
ヒープはプログラマがnewやdeleteで明示的に管理する領域で、必要なときに必要な量だけ確保できます。
動的メモリとは主にヒープ領域のメモリのことを指します。
次の表は両者の違いをまとめたものです。
項目 | スタック | ヒープ |
---|---|---|
管理方式 | 自動(関数の入退出で増減) | 手動(new/delete) |
生存期間 | スコープ終了まで | deleteするまで |
サイズ制約 | 比較的小さい | 大きめ(ただしOS依存) |
速度 | 非常に速い | 相対的に遅い |
用途 | 一時的な自動変数 | 長期間保持するデータ |
典型エラー | スタックオーバーフロー | メモリリーク、二重解放 |
本稿では、ヒープ上の単一オブジェクトをnewとdeleteで確保・解放する基本に集中します(配列のnew[]/delete[]は別稿で扱います)。
newで単一オブジェクトを確保する
newはヒープからメモリを確保し、型に応じてコンストラクタ呼び出しや初期化を行います。
単一のintを確保する簡単な例を見てみます。
#include <iostream>
int main() {
// 単一のintを動的確保し、値を設定
int* p = new int(42); // 値42で初期化
std::cout << "pのアドレス: " << p << ", 値: " << *p << '\n';
// 後始末
delete p; // メモリを解放
p = nullptr; // ダングリングポインタ防止
// nullptrは解放済みを表し、デリファレンスしてはいけない
if (p == nullptr) {
std::cout << "pはnullptrです\n";
}
return 0;
}
pのアドレス: 0x55f4..., 値: 42
pはnullptrです
クラス型をnewする場合はコンストラクタが呼ばれ、オブジェクトの初期化が行われます。
deleteで確実にメモリ解放
deleteはnewで確保した単一オブジェクトを解放します。
クラス型ではデストラクタが呼ばれ、その後メモリが返却されます。
deleteは1回だけ行い、解放後は必ずポインタにnullptrを代入して、解放済み領域へのアクセス(ダングリングポインタ)を防ぎます。
配列をnew[]で確保した場合はdelete[]で解放する必要がありますが、これは別の記事で詳説します。
malloc/freeとの違い(コンストラクタと型安全)
C言語のmalloc/freeとC++のnew/deleteは似た用途でも動作と安全性が大きく異なります。
観点 | new/delete | malloc/free |
---|---|---|
初期化 | コンストラクタ/デストラクタ呼び出し | なし(生メモリのみ) |
型安全 | 型が決まる。戻り値はその型のポインタ | void*を返す(キャスト必要) |
失敗時 | 例外(std::bad_alloc)を投げる | NULLを返す(戻り値チェック必要) |
組み合わせ | new↔deleteのみ | malloc↔freeのみ |
使いどころ | C++のオブジェクト管理 | C互換APIなど特殊用途 |
コードで違いを体感します。
#include <iostream>
#include <cstdlib> // malloc, free
#include <new> // 例外型の宣言用
class Widget {
public:
Widget() : x(123) { std::cout << "Widget::ctor, x=" << x << '\n'; }
~Widget() { std::cout << "Widget::dtor\n"; }
private:
int x;
};
int main() {
// new/delete: コンストラクタ/デストラクタが呼ばれる
Widget* w1 = new Widget();
delete w1;
// malloc/free: コンストラクタは呼ばれない
void* raw = std::malloc(sizeof(Widget));
if (!raw) {
std::cerr << "malloc失敗\n";
return 1;
}
std::cout << "mallocで確保しただけ: コンストラクタは未呼び出し\n";
// 注意: 以下は未定義動作の例なので実行しないこと
// Widget* w2 = static_cast<Widget*>(raw); // オブジェクト未構築
// delete w2; // mallocとdeleteの混用はNG
std::free(raw); // mallocしたものはfreeする
return 0;
}
Widget::ctor, x=123
Widget::dtor
mallocで確保しただけ: コンストラクタは未呼び出し
new/deleteとmalloc/freeの混用は未定義動作です。
必ずペアを守ってください。
ポインタ管理のルール(安全なdelete)
ポインタはnullptrで初期化
未初期化ポインタは不定値を持ち、誤って使うと致命的なバグにつながります。
宣言時に必ずnullptrで初期化します。
nullptrは「どこも指していない」ことを表す安全な値です。
delete後はnullptrを代入
delete後に同じポインタを再度使ってしまうのを防ぐため、即座にnullptrを代入します。
これにより、誤って再利用した場合でも即座に検出しやすくなります。
#include <iostream>
int main() {
int* p = nullptr; // 安全な初期化
delete p; // nullptrへのdeleteは安全(何もしない)
std::cout << "nullptrをdeleteしても安全です\n";
p = new int(7);
delete p; // 解放
p = nullptr; // ダングリング防止
delete p; // 再deleteも安全
std::cout << "解放後にnullptr代入→再deleteも安全\n";
}
nullptrをdeleteしても安全です
解放後にnullptr代入→再deleteも安全
メモリリークの原因と対策(deleteのし忘れ)
メモリリークは、確保したメモリを解放し忘れたまま参照を失い、再利用不能になることです。
早期returnや例外によってdeleteがスキップされるのが典型的な原因です。
#include <iostream>
void leaky(bool earlyReturn) {
std::cout << "[leaky] start\n";
int* p = new int(10); // 確保
if (earlyReturn) {
std::cout << "[leaky] 途中でreturn。deleteを忘れてリーク!\n";
return; // delete p; を忘れている
}
// ... 他の処理 ...
delete p; // ここまで来れば解放
std::cout << "[leaky] end\n";
}
void fixed(bool earlyReturn) {
std::cout << "[fixed] start\n";
int* p = new int(10);
if (earlyReturn) {
delete p; // 早期return前に必ず解放
std::cout << "[fixed] 途中でreturn。delete済み\n";
return;
}
delete p;
std::cout << "[fixed] end\n";
}
int main() {
leaky(true); // リークする経路
fixed(true); // 解放する経路
}
[leaky] start
[leaky] 途中でreturn。deleteを忘れてリーク!
[fixed] start
[fixed] 途中でreturn。delete済み
例外対応まで含めた漏れ防止は後述のRAII(コンストラクタでnew、デストラクタでdelete)で根本的に解決します。
二重解放とダングリングポインタを避ける
同じポインタに対して2回deleteを呼ぶと未定義動作です。
また、delete後のポインタを使うのはダングリング参照で、非常に危険です。
delete直後にnullptr代入することで再利用を防げます。
複数箇所で同じポインタを共有しない設計(所有権を1箇所に限定)も有効です。
deleteはnullptrに安全
deleteはnullptrに対して呼び出しても何も起きません。
そのため、以下のパターンは安全です。
// 典型パターン
delete p;
p = nullptr;
delete p; // 2回目でも安全(nullなので何もしない)
例外とエラー対策(newの失敗)
newはstd::bad_allocを投げる
newは原則として確保に失敗したときstd::bad_alloc例外を投げます。
一般的なプログラムではメモリ不足がそう頻繁に起きるわけではありませんが、例外を捕捉してメッセージを出すなどの対策ができます。
#include <iostream>
#include <new> // std::bad_alloc
int main() {
try {
int* p = new int(99); // 通常は成功
std::cout << "new成功: " << *p << '\n';
delete p;
} catch (const std::bad_alloc& e) {
std::cerr << "new失敗: " << e.what() << '\n';
}
}
new成功: 99
確保に失敗した場合は「new失敗: std::bad_alloc」などが表示されます。
nothrow newの使い方と戻り値チェック
例外を使いたくない場面では、nothrow版のnewを使うと、失敗時にnullptrが返ります。
戻り値を必ずチェックしてください。
#include <iostream>
#include <new> // std::nothrow
int main() {
int* q = new (std::nothrow) int(123); // 失敗時はnullptr
if (!q) {
std::cerr << "nothrow版: 確保失敗でnullptr\n";
return 1;
}
std::cout << "nothrow版: 確保成功: " << *q << '\n';
delete q;
}
nothrow版: 確保成功: 123
例外時にdeleteを確実に行う設計
例外が発生してもメモリを確実に解放するには、RAII(Resource Acquisition Is Initialization)を使います。
具体的には、コンストラクタでnew、デストラクタでdeleteし、オブジェクトをスコープに置くことで、スコープを抜ける際に自動で後始末できます。
#include <iostream>
#include <stdexcept>
class Holder {
public:
Holder() : p(new int(42)) { std::cout << "Holder() でnew\n"; }
~Holder() { std::cout << "~Holder() でdelete\n"; delete p; }
int value() const { return *p; }
private:
int* p;
};
void mayThrow(bool doThrow) {
Holder h; // スタックに生成。例外時も必ずデストラクタが走る
std::cout << "値: " << h.value() << '\n';
if (doThrow) {
std::cout << "例外を投げます\n";
throw std::runtime_error("oops");
}
std::cout << "正常終了\n";
}
int main() {
try {
mayThrow(true); // 例外を投げる経路
} catch (const std::exception& e) {
std::cout << "catch: " << e.what() << '\n';
}
}
Holder() でnew
値: 42
例外を投げます
~Holder() でdelete
catch: oops
例外が起きてもデストラクタが確実に呼ばれ、メモリが解放されていることがわかります。
実践パターンとベストプラクティス(new/delete)
コンストラクタでnew、デストラクタでdelete
クラスが内部で動的メモリを使う場合は、その確保と解放をクラスの責務として閉じ込めます。
ユーザはdeleteを意識せず、スコープ制御だけで安全に使えます。
#include <iostream>
class NumberBox {
public:
explicit NumberBox(int v) : ptr(new int(v)) {
std::cout << "NumberBox: newで" << v << '\n';
}
~NumberBox() {
std::cout << "NumberBox: delete\n";
delete ptr;
}
int get() const { return *ptr; }
private:
int* ptr;
};
int main() {
NumberBox box(7); // スコープを抜けると自動でdelete
std::cout << "中身: " << box.get() << '\n';
}
NumberBox: newで7
中身: 7
NumberBox: delete
関数間の所有権を明確にする
関数間でポインタを受け渡すときは、「誰がdeleteするか(所有権)」をコードとコメントで明確にします。
所有権を持つ側は必ずdeleteし、所有権を持たない側はdeleteしません。
#include <iostream>
int* createInt(int v) { // 所有権を呼び出し側に移譲
return new int(v);
}
void printInt(const int* p) { // 所有権は受け取らない(解放しない)
if (p) std::cout << "値: " << *p << '\n';
}
void takeOwnership(int* p) { // 所有権を受け取り、ここで解放
std::cout << "takeOwnership: " << *p << '\n';
delete p; // 以後呼び出し側は使わない約束
}
int main() {
int* a = createInt(10); // aの所有権はmain
printInt(a); // 使うだけ
delete a; // mainで解放
int* b = createInt(20); // bの所有権を関数に移譲
takeOwnership(b);
b = nullptr; // ダングリング防止(以後使わない)
}
値: 10
takeOwnership: 20
所有権の取り扱いを明示すると、二重解放やリークの予防に直結します。
new/deleteを対で置く(同一スコープ内)
可能な限り、newとdeleteを同じ関数や同じブロックの近い位置に置き、見通しを良くします。
ロジックが複雑になるほどdelete漏れのリスクが高まるためです。
#include <iostream>
void localized() {
std::cout << "localized開始\n";
int* p = new int(5); // new
std::cout << "値: " << *p << '\n';
delete p; // deleteを近くに置く
p = nullptr; // 明示的に無効化
std::cout << "localized終了\n";
}
// 悪い例: newとdeleteが離れて見失う(説明のためコメントのみ)
void antiPattern() {
// int* p = new int(5);
// if (条件) return; // 早期returnでdelete漏れの危険
// ... // 複雑なロジックの後
// delete p;
}
int main() {
localized();
}
localized開始
値: 5
localized終了
new/deleteを同一スコープ内に保てない設計が多い場合は、所有権の整理やクラスへのカプセル化を検討します。
まとめ
本稿では、C++におけるnewとdeleteの基本から、ポインタの安全な扱い、例外時の確実な後始末、所有権設計のベストプラクティスまで、単一オブジェクトの動的メモリ管理を丁寧に解説しました。
重要な要点は次のとおりです。
newは型安全でコンストラクタを呼び、deleteはデストラクタを呼んで解放します。
delete後はnullptr代入でダングリングを防ぎ、nullptrへのdeleteは安全です。
例外対策としてはRAIIを徹底し、コンストラクタでnew、デストラクタでdeleteを行うクラス設計で漏れを根絶できます。
関数間でポインタを受け渡す場合は所有権を明確にし、可能な限りnew/deleteを同一スコープに置くことが堅牢性につながります。
なお、配列のnew[]/delete[]やスマートポインタによる自動管理などは別の記事で扱いますが、今回の基礎を理解することで、それらの設計思想も格段に理解しやすくなります。