Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

スタックとヒープ

所有権を理解するために、まずメモリの仕組みを学びます。

メモリの2つの領域

プログラムが使うメモリには主に2つの領域があります。

┌─────────────────────────────────────────┐
│                メモリ                    │
│  ┌─────────────┐  ┌─────────────────┐   │
│  │   スタック   │  │     ヒープ       │   │
│  │  (Stack)    │  │    (Heap)       │   │
│  │             │  │                 │   │
│  │ 高速・固定   │  │ 柔軟・可変       │   │
│  │ サイズ限定   │  │ サイズ自由       │   │
│  └─────────────┘  └─────────────────┘   │
└─────────────────────────────────────────┘

スタック(Stack)

特徴

  • LIFO(Last In, First Out):最後に入れたものを最初に取り出す
  • 高速:メモリの場所が決まっている
  • 固定サイズ:コンパイル時にサイズがわかるデータのみ

イメージ:本の積み重ね

      ↓ push(追加)
   ┌─────┐
   │  3  │ ← 最後に追加
   ├─────┤
   │  2  │
   ├─────┤
   │  1  │ ← 最初に追加
   └─────┘
      ↑ pop(取り出し)は上から

スタックに置かれるデータ

fn main() {
    let x: i32 = 42;      // スタック(サイズ固定:4バイト)
    let y: f64 = 3.14;    // スタック(サイズ固定:8バイト)
    let z: bool = true;   // スタック(サイズ固定:1バイト)
    let arr: [i32; 3] = [1, 2, 3];  // スタック(サイズ固定:12バイト)
}

これらはコンパイル時にサイズがわかるので、スタックに置かれます。

ヒープ(Heap)

特徴

  • 動的:実行時にサイズが決まるデータを置ける
  • 柔軟:大きなデータや可変長データを扱える
  • 間接アクセス:ポインタ経由でアクセス

イメージ:倉庫と伝票

スタック(伝票)          ヒープ(倉庫)
┌─────────┐            ┌─────────────────┐
│ ptr ────────────────→│ "Hello, Rust!"  │
│ len: 13 │            └─────────────────┘
│ cap: 13 │
└─────────┘

ヒープに置かれるデータ

fn main() {
    let s = String::from("Hello");  // ヒープに文字列データ
    let v = vec![1, 2, 3, 4, 5];    // ヒープに配列データ
}

StringVec実行時にサイズが変わる可能性があるので、ヒープに置かれます。

Stringのメモリレイアウト

#![allow(unused)]
fn main() {
let s = String::from("hello");
}
スタック                     ヒープ
┌─────────────┐            ┌───┬───┬───┬───┬───┐
│ ptr ─────────────────────→│ h │ e │ l │ l │ o │
├─────────────┤            └───┴───┴───┴───┴───┘
│ len: 5      │            (5バイトのデータ)
├─────────────┤
│ capacity: 5 │
└─────────────┘
  • ptr: ヒープ上のデータへのポインタ
  • len: 現在の長さ
  • capacity: 確保済みの容量

なぜ2つの領域があるのか

スタックの限界

#![allow(unused)]
fn main() {
// これはコンパイルエラー(サイズが不明)
// let s: str = ???;  // strのサイズは実行時まで不明
}

スタックには「コンパイル時にサイズがわかるもの」しか置けません。

ヒープの必要性

fn main() {
    let mut s = String::from("Hello");
    s.push_str(", World!");  // 文字列が伸びる
    println!("{}", s);
}

Stringは後から長さが変わるので、柔軟なヒープに置く必要があります。

速度の違い

アクセス速度の比較(イメージ):

スタック: ████████████████████ 100%
ヒープ:   ██████████           50%(ポインタ経由のため)

スタックは「次のデータの場所」が決まっているので高速です。 ヒープは「ポインタをたどる」必要があるため、少し遅くなります。

コピーとムーブ

スタックのデータ:コピー

fn main() {
    let x = 5;
    let y = x;  // xの値をコピー

    println!("x = {}, y = {}", x, y);  // 両方使える
}

スタックのデータは小さいので、そのままコピーされます。

ヒープのデータ:ムーブ

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1からs2にムーブ

    // println!("{}", s1);  // エラー!s1は無効
    println!("{}", s2);     // OK
}

ヒープのデータは大きい可能性があるので、所有権が移動します。 これが次のドキュメントで学ぶ「所有権」の核心です。

まとめ

特徴スタックヒープ
サイズ固定(コンパイル時に決定)可変(実行時に決定)
速度高速比較的遅い
データ例i32, f64, bool, 配列String, Vec
代入時コピームーブ(所有権移動)

確認テスト

Q1. スタックに置かれるデータの特徴として正しいものはどれですか?

Q2. String型がヒープにデータを置く理由として正しいものはどれですか?

Q3. 以下のコードで、ヒープにデータが確保される変数はどれですか?
let a: i32 = 10; let b = String::from("hi"); let c: [i32; 3] = [1,2,3];

Q4. 以下のコードがコンパイルエラーになる理由は何ですか?
let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);

Q5. let x = 5; let y = x;の実行後、xとyの状態として正しいものはどれですか?


次のドキュメント: 02_ownership.md