コンテンツにスキップ

【比較】スタックとヒープの違い

概要

プログラムが実行時に使うメモリは、用途に応じていくつかの領域に分かれます。その中でも「スタック(stack)」と「ヒープ(heap)」は、ローカル変数や動的に確保したオブジェクトが置かれる代表的な2つの領域です。スタックは関数呼び出しに同期して自動的に確保・解放され、ヒープは必要なときに明示的(または GC によって)に確保・解放されます。C 言語規格の用語で言えば、前者は「自動記憶域期間(automatic storage duration)」、後者は「割り付け記憶域期間(allocated storage duration)」に対応します。

混乱しやすい最大の原因は、「スタック」という言葉が2つの別物を指すことです。1つは LIFO(後入れ先出し)で push/pop する抽象データ構造としてのスタック、もう1つは関数の呼び出し履歴とローカル変数を格納するメモリ領域としてのコールスタックです。後者が「スタック」と呼ばれるのは LIFO 構造をメモリ管理に応用しているからですが、両者は別概念です。本記事は主にメモリ領域としてのスタックとヒープを扱い、必要な箇所でデータ構造との区別を明示します。

例えるなら:使い捨てトレイと貸し倉庫

スタックは社員食堂のトレイの山に似ています。料理を取るとき一番上のトレイを取り、返すときも上に重ねる(LIFO)。誰がいつ片付けるかは決まっていて(関数が戻る=自動返却)、置ける枚数の上限も決まっています(スタックサイズ上限)。一方ヒープは貸し倉庫です。好きなサイズの区画を好きなタイミングで借り、不要になったら自分で返却手続きをします(free/delete、または GC が代行)。返し忘れると区画が埋まり続け(メモリリーク)、借りた区画があちこちに散らばると大きな連続区画が取れなくなります(断片化)。この比喩は管理主体と寿命の違いを掴むのに有効ですが、実際のアドレス配置やページングは比喩より複雑であることに注意してください。

詳細比較

項目 スタック (Stack) ヒープ (Heap)
対応する記憶域期間 自動記憶域期間(automatic)。ブロック/関数のスコープに束縛 割り付け記憶域期間(allocated)。明示的に確保・解放するまで存続
確保・解放の主体 コンパイラ/ランタイムが自動。関数の入退場で確保・破棄 プログラマが明示(C は malloc/free、C++ は new/delete)。GC 言語ではランタイムが回収
割り当て速度 非常に速い(スタックポインタの加減算のみ)。alloca(3) は「malloc(3)/free(3) より速い」と man に明記 相対的に遅い(空き領域の探索・管理構造の更新が必要)
寿命 スコープを抜けると即解放。戻り値のローカル変数のアドレスを返すと未定義動作 解放するまで(または GC が到達不能と判断するまで)任意に存続。関数を跨いで保持できる
サイズの上限 スレッドごとに固定上限。Windows の既定予約は 1 MB、Linux/NPTL は RLIMIT_STACK 由来(ディストリ既定の ulimit -s では 8 MB が一般的;man7 が規定するのは unlimited 時のアーキ依存値 2〜4 MB) プロセスの仮想アドレス空間と OS のメモリが許す限り。実行時に拡張可能
超過時の挙動 スタックオーバーフロー。Windows はガードページ、Linux は典型的に SIGSEGV。alloca は失敗を通知できず SIGSEGV になり得る 確保失敗。malloc は NULL を返し、C++ の new は既定で std::bad_alloc を送出(エラー検出が可能)
断片化 起きない(LIFO で末尾のみ伸縮するため隙間ができない) 起き得る(確保・解放が任意順なので外部断片化で連続空きが枯渇し得る)
典型的な内容 関数のスタックフレーム(戻りアドレス、引数、ローカル変数、退避レジスタ) 動的配列、可変長コレクション、寿命がスコープを超えるオブジェクト

スタックは関数呼び出しのたびに「スタックフレーム(活性化レコード)」を積み、戻ると同時にフレームごと破棄されます。この LIFO 構造ゆえに割り当てはポインタの移動だけで完了し、断片化も原理的に発生しません。代償として、サイズ上限が固定で、関数を跨いでデータを生存させられません。ヒープはこの制約を取り払う代わりに、解放責任(または GC コスト)と断片化リスクを引き受けます。

よくある誤解

  • 「データ構造のスタックとメモリのスタックは同じもの」は誤り(言葉の混同)。 スタックという抽象データ構造は LIFO で push/pop する操作的定義であり、配列・連結リストなど任意の方法で実装できます。一方コールスタックは実行中の関数呼び出しとローカル変数を保持するメモリ領域で、たまたまその管理規律が LIFO だからスタックと呼ばれています。「ヒープ(heap)」も同様で、メモリ領域としてのヒープは、優先度付きキューを実装する木構造データ構造の「ヒープ」とは無関係の別概念です(名前が衝突しているだけ)。

  • 「ヒープはスタックより常に遅いから使うべきでない」は誤った一般化。 確かに 1 回の割り当てコストはヒープの方が高いですが、スタックには「スレッドごとの固定上限(Windows 既定 1 MB 等)」「スコープを超えて生存できない」という本質的制約があります。巨大なバッファや、関数の生存期間を超えて保持するデータをスタックに置こうとすると、スタックオーバーフローやダングリングポインタを招きます。サイズが大きい・寿命が長い・実行時までサイズ不明、のいずれかならヒープが正しい選択です。

  • 「GC のある言語(Java/Python/Go 等)にはスタックとヒープの区別はない」は誤り。 GC が自動化するのはヒープの解放であって、領域そのものはなくなりません。これらの言語でもメソッドのローカル変数やフレームはスタック上にあり、オブジェクト本体はヒープに置かれて GC の管理対象になります。スタックオーバーフロー(深い再帰など)も依然として起こります。

実務での選び分け

  • 小さく・寿命が短く・サイズがコンパイル時に決まる → スタック(ローカル変数で素直に書く。最速で解放漏れもない)。
  • 大きい/実行時までサイズ不明/関数の寿命を超えて保持する → ヒープ(malloc/new、または GC 言語のオブジェクト)。
  • 深い再帰や巨大なローカル配列でスタックを圧迫しそう → 再帰をループ化する、または明示的にヒープへ退避する。スレッドのスタックサイズを増やすのは最後の手段。
  • C/C++ で手動ヒープを使うと決めたら、確保とペアで解放戦略を最初に設計 → RAII(C++)、所有権の明確化、malloc の戻り値 NULL チェックを習慣化する。
  • alloca / 可変長配列(VLA)は慎重に → 速いが失敗を通知できず(man に明記)、サイズが入力依存だと容易にスタックオーバーフローする。サイズが大きい・不定なら malloc を選ぶ。

ひとことまとめ

スタックは「関数のスコープに同期して自動で確保・解放される高速だがサイズ上限のある領域」、ヒープは「明示的(または GC)に管理し寿命とサイズを自由にできる代わりに解放責任と断片化を伴う領域」であり、データの寿命とサイズが選択を決めます。

出典・参考