コンピュータサイエンス

スタック(Stack) vs ヒープ(Heap):変数の保存場所を完全整理(Swiftの値型まで)

メモリのスタックは関数呼び出しとともに片付き、ヒープは動的な寿命とサイズを支えます。2つの領域のコスト、Swiftの値型・参照型の実際の配置、データ構造としてのStack・Heapとの違いを整理します。

読了 5 分
スタック(Stack) vs ヒープ(Heap):変数の保存場所を完全整理(Swiftの値型まで)のカバー画像

変数を宣言すると、その値はどこに保存されるのでしょうか。

「メモリです」と答えるのは半分正解ですが、十分ではありません。同じメモリでも、スタックに置かれる値とヒープに置かれる値では、そのライフサイクルがまったく異なります。

スタックオーバーフローというエラー名、クラスと構造体の性能差、ARCが必要な理由まで、すべてこの2つの領域を知れば説明できます。

この記事では、スタックとヒープの動作、速度差が生じる理由、Swiftで何がどこに置かれるのかを整理します。

ここで指すのはメモリ領域です。LIFOデータ構造はSwiftのスタックとキューで、優先度付きキューを実装する木構造はヒープと優先度付きキューで別途扱います。

まず要点をまとめます。

  1. スタック:関数呼び出しで積まれ、リターンとともに消える自動管理領域。高速。
  2. ヒープ:実行時に必要なサイズだけ借りて使う動的領域。柔軟ですが管理コストがかかる。
  3. 速度差の本質は「割り当て・解放の方法」と「誰が後片付けをするか」です。
  4. Swiftでは、構造体はおおむねスタックに、クラスのインスタンスはヒープに置かれます。

スタック:積み重ねた皿

スタックは関数呼び出しの情報を積む領域です。名前どおり皿を積むように、最後に置いたものを最初に取り出すLIFO(後入れ先出し)構造です。

関数を呼び出すと、ローカル変数、引数、リターンアドレスがひとまとまりのスタックフレームとして積まれます。関数がリターンすると、そのフレーム全体が消えます。

ここにスタックの強みがあります。

割り当てが高速です。スタックポインターを上へ動かせば完了します。空き領域を探す必要はありません。

解放も無料です。関数が終わったらポインターを戻すだけで済みます。誰が片付けるか悩む必要もありません。

ただし制約があります。サイズはコンパイル時に決まっている必要があり、関数が終われば必ず消え、全体の上限も小さい(通常数MB)ことです。終了条件のない再帰関数が自分自身を呼び続けると、フレームが上限を超えます。それがスタックオーバーフローです。


ヒープ:広いが管理が必要な倉庫

ヒープは実行時に必要な分だけ借りて使う領域です。サイズを事前に知らなくてもよく、関数終了後も残り、容量もはるかに大きくなります。

その代わり、管理コストがかかります。

割り当ては相対的に遅くなります。要求サイズに合う空き領域を探す必要があり、複数スレッドが同時に要求すれば調整も必要です。

解放は誰かが責任を負う必要があります。関数終了後も消えないため、「もう使わない」と判断する役割が必要です。Cではプログラマーがfreeを直接呼び、Javaではガベージコレクターが定期的に掃除し、SwiftではARCが参照カウントを数え、0になった瞬間に解放します。

判断を誤ると2つの問題が起きます。解放が遅すぎればメモリーリーク、早すぎれば解放済みメモリーへのアクセス(ダングリングポインター)です。

スタックフレームの参照がヒープ上の実体オブジェクトを指す構造
スタックフレームの参照がヒープ上の実体オブジェクトを指す構造

Swiftでは何がどこに置かれるのか

Swiftの公式ドキュメントやWWDCセッションでは、次の構図が繰り返し強調されています。値型(構造体・列挙型)はスタックに、参照型(クラス)はヒープに割り当てられるのが基本です。

struct PointStruct { var x, y: Double }
class PointClass { var x = 0.0, y = 0.0 }

func run() {
    let a = PointStruct(x: 1, y: 2) // スタックフレーム内に丸ごと
    let b = PointClass()            // ヒープに割り当て、スタックには参照だけ
}

構造体aは値が丸ごとスタックフレームに入ります。関数終了時にフレームとともに消えるため、参照カウントも不要です。

クラスインスタンスbはヒープに置かれ、スタックにはアドレスだけが置かれます。複数箇所から参照できるため、ARCがカウントを管理します。

Appleが「まず構造体を検討する」よう勧める性能面の根拠がこれです。ヒープ割り当て、参照カウント、ロックのコストをまとめて省けます。

ただし「構造体=必ずスタック」ではありません。クラスのプロパティになった構造体はクラスとともにヒープに置かれ、StringやArrayのような型は外見こそ構造体ですが、実データのバッファーをヒープに持ちます。「構造体はスタックに置ける条件を備えている」と理解するのが正確です。

構造体はスタックに丸ごと置かれ、クラスは本体をヒープに置いて参照だけをスタックに置く
構造体はスタックに丸ごと置かれ、クラスは本体をヒープに置いて参照だけをスタックに置く

面接でよくある追加質問

「なぜスタックはヒープより速いのですか?」 — スタックはポインターの移動だけで割り当て・解放が終わりますが、ヒープでは空き領域の探索と解放管理(参照カウント・GC)が必要だからです。

「なぜスタックオーバーフローが起きるのですか?」 — スタックの上限は小さいため、深い再帰や巨大なローカル変数でフレームが上限を超えると発生します。

「ローカル変数は必ずスタックに置かれますか?」 — いいえ。ローカル変数でも参照型なら、本体はヒープにあります。スタックに置かれるのは参照だけです。


まとめ

  • スタックは関数呼び出し単位で自動的に積まれ、消える領域。ポインター移動だけで管理されるため高速。
  • ヒープは実行時にサイズを決めて借りる領域。柔軟ですが、割り当て探索と解放管理のコストがかかる。
  • 解放の責任:スタックは自動、ヒープは言語ごとに異なる(Cは手動、JavaはGC、SwiftはARC)。
  • スタックオーバーフローは、深い再帰などでスタックの上限を超えると発生するエラー。
  • Swiftでは構造体はおおむねスタック、クラスはヒープ。これが値型を優先する性能上の根拠です。
  • ただしクラス内の構造体はヒープに置かれ、String・Arrayは内部バッファーをヒープに持ちます。