なぜ実行時にメモリを要求するのか
これまで作ってきた変数はすべてスタック上に存在していました。サイズはコンパイル時に決まり、スコープが終われば自動的に破棄されます。これは高速で安全ですが、プログラムが動き出すまで必要なメモリ量が分からない場合には対応できません。たとえば、ユーザー入力でサイズが決まるバッファ、それを作った関数より長く生き残る必要のある構造、あるいは形が事前に分からないグラフなどです。
こうした場合のために、C++ではnewでヒープ(フリーストアとも呼ばれます)から確保し、deleteで返却できます。new式はブロックを予約し、コンストラクタを実行し、それを指すポインタを返します。これは前のページで見たポインタの上に直接成り立っています。
変数p自体はスタック上に存在し、ただのポインタです。それが指すintはヒープ上に存在し、いくつスコープが入れ替わろうと、deleteするまで生き続けます。
スタックとヒープ
この区別こそがnewが存在する理由そのものなので、具体的にしておく価値があります。
void demo() {
int a = 10; // スタック上 - demo() が返ると消える
int* b = new int(10); // 'b' はスタック上、それが指す int はヒープ上
} // 'a' は破棄される。ヒープ上の int はリークする - 決して delete されない
主な違い:
- スタック - 自動的な寿命、非常に高速、サイズに制限あり(通常は数MB)、スコープを抜けると自動的に解放される。
- ヒープ - 手動の寿命、わずかに低速、大きく、
deleteを呼んだときのみ解放される。
このトレードは責任と引き換えに柔軟性を得るものです。ヒープメモリは望むだけきっかり生き続けますが、それを解放するのを忘れないようにする責任はあなたが負うことになります。
new[] で配列を確保する
長さが実行時に決まるブロックが必要なときは、配列形式のnew T[n]を使います。これは先頭要素へのポインタを返し、対応するdelete[]で解放します。
このルールは厳格で間違えやすいものです。newから得たメモリはdeleteで、new[]から得たメモリはdelete[]で解放します。これらを混ぜること(new[]で確保したものにdelete arrを使うこと)は、たとえあなたのマシンで動いているように見えても未定義動作です。
3つの典型的なバグ
手動のメモリ管理には、ヒープバグの大半を占める少数の誤りがあります。3つすべてを見分けられるようになりましょう。
1. メモリリーク - deleteを一度も呼ばない。 ブロックは永遠に確保されたままになります。1回なら無害、ループ内なら致命的です。
void leaky() {
int* p = new int(5);
// ... delete がない ...
} // p は消える。ヒープ上の int は到達不能になり、かつ解放もされない
2. ダングリングポインタ - メモリを解放した後に使う。 ポインタはまだ古いアドレスを保持していますが、そのメモリはもうあなたのものではありません。
3. 二重解放 - 同じブロックを2回deleteする。 これはヒープの内部管理情報を壊し、たいていクラッシュします。
int* p = new int(1);
delete p;
delete p; // 二重解放 - 未定義動作、しばしばクラッシュ
削除後にポインタをnullptrに設定すると、ダングリング使用と二重解放の両方を無力化できます。nullptrのデリファレンスは即座にクラッシュし(デバッグしやすい)、delete nullptrは明示的に安全な何もしない操作だからです。
現実的な「確保・使用・解放」サイクル
すべてをまとめると、正しい手動管理の形は次のようになります。確保し、使い、ちょうど1回だけ解放し、その後はポインタに触れないことです。
クラス型の場合、delete uが2つのことを行う点に注意してください。まずオブジェクトのデストラクタを実行し、それから生のメモリを解放します。この順序は、オブジェクトが自身のリソースを所有するようになると重要になります。
微妙な落とし穴:newとdeleteの間で例外が投げられると、deleteは決して実行されず、リークが起きます。これに対処するためにすべての確保をtry/catchで包むのは面倒でミスを招きやすく、まさに次のページが解決する問題です。
次へ:スマートポインタ
ここまでで、メモリを手作業で扱うことの全コストを見てきました。すべてのnewは後でdeleteするという約束であり、たった1つの忘れ・重複・早すぎる解放が未定義動作になります。現代のC++はこの約束を手動で行うことはほとんどありません。次のページではスマートポインタを紹介します。std::unique_ptrとstd::shared_ptrは、ヒープ確保を所有し、スコープを抜けるときにあなたの代わりに自動的にdeleteを呼ぶオブジェクトで、3つの典型的なバグをコンパイラとRAIIが代わりに処理してくれるものへと変えてくれます。
よくある質問
C++におけるnewとdeleteの違いは何ですか?
newは実行時にヒープ上にメモリを確保し、それを指すポインタを返します。deleteはnewで確保したメモリを解放します。すべてのnewはちょうど1つのdeleteと対応させなければならず、そうしないとメモリリークが起きます。配列の場合はnew[]とdelete[]を使います。
C++でdeleteの呼び出しを忘れるとどうなりますか?
メモリリークが起きます。もう何もそれを指していなくても、ヒープのブロックはプログラムの実行中ずっと確保されたままになります。1回のリークはたいてい無害ですが、ループ内や長時間動作するサービスでのリークは積み重なり、最終的にメモリが足りなくなってプログラムがクラッシュします。
現代のC++でnewとdeleteを直接使うべきですか?
めったに使いません。メモリを自動的に解放してくれるstd::vectorのようなコンテナや、スマートポインタ(std::unique_ptr、std::shared_ptr)を優先しましょう。スマートポインタは内部でnew/deleteをラップしているため、生のnew/deleteを理解しておく価値はありますが、日常のコードではリークやダングリングポインタの原因になります。