C++での動的メモリ確保は、プログラムの柔軟性を高める一方でミスが起きやすい箇所でもあります。
本稿ではnewとdeleteを中心に、安全に確保と解放を行うための基本と作法を、初心者向けに順を追って解説します。
配列版のnew[]
とdelete[]
の対応や、二重解放・ミスマッチなどの落とし穴にも丁寧に触れます。
C++の動的メモリの基礎(new, delete)
動的メモリとは
動的メモリとは、new
で実行時に必要な分だけ確保し、delete
で任意のタイミングで解放する領域のことです。
必要な大きさが実行時まで分からないデータを扱うときに有効です。
静的領域や自動変数(ローカル変数)とは寿命や管理方法が異なります。
いつ使うか
入力サイズに応じて配列の大きさが変わる、GUIのウィジェットを動的に生成する、などの場面で使われます。
固定サイズで済むならローカル変数(スタック)を優先し、必要なときだけ動的メモリを検討します。
スタックとヒープの違い(かんたん解説)
スタックは関数呼び出しに伴って自動で確保・解放される領域で、高速ですがサイズや寿命がスコープに縛られます。
ヒープはnew
とdelete
で手動管理する領域で、柔軟ですがミスが起きやすいです。
速度・安全のスタック、柔軟性のヒープと覚えると良いでしょう。
以下は対比の表です。
観点 | スタック | ヒープ |
---|---|---|
確保/解放 | 自動 | 手動(new/delete) |
速度 | 速い | 相対的に遅い |
寿命 | スコープ終了まで | deleteするまで |
使いどころ | 固定サイズ、短寿命 | 可変サイズ、長寿命 |
ポインタとnullptrの基本
new
はメモリアドレスを返すので、変数にはポインタ型を使います。
未初期化ポインタは危険なので、必ずnullptr
で初期化しましょう。
nullptr
は「どこも指していない」ことを意味します。
例: ポインタの初期化
#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など)はデフォルトでは未初期化です。
未初期化値の読み取りは未定義動作のため、割り当ててから使います。
#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{...}
が使えます。
#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++の利点です。
#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 intn
やnew int[n]{}
でゼロ初期化できます。
#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
を代入してダングリングを防ぎます。
#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[]
で解放します。
クラス型配列では各要素のデストラクタが順番に呼ばれます。
#include <iostream>
int main() {
int* a = new int[3]{1, 2, 3};
delete[] a; // 忘れずに [] を付ける
}
deleteとdelete[]のミスマッチに注意
単一オブジェクトにdelete[]
、配列にdelete
を使うのは未定義動作です。
new と delete、new[] と delete[] を必ず対応させます。
// これはダメな例(実行しないでください)
// int* p = new int{42};
// delete[] p; // × 未定義動作
// int* a = new int[3]{1,2,3};
// delete a; // × 未定義動作
delete後はnullptrを代入(二重解放を防ぐ)
解放直後にptr = nullptr;
を徹底すると、誤って再度delete
してしまう二重解放を抑止できます。
#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
を呼ぶようにします。
例: 関数内で必ず解放する
#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つ、他は借用(非所有)と決め、コメントや変数名で明示します。
// 所有者: owner、借用: view
int* owner = new int{5};
int* view = owner; // 借用: 解放しない約束
// ... view は読み取り専用として使う ...
delete owner; // 解放は所有者のみ
owner = nullptr;
view = nullptr; // 以降は使わない
早期returnでもdeleteを忘れない
条件分岐で早期にreturn
すると解放を忘れがちです。
エラー分岐の直前で解放するか、最後に集約して解放しましょう。
#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
を代入し、その後の使用を検出できるようにします。
#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 の正しい理解がコードの健全性を支えます。