スタックとヒープ
所有権を理解するために、まずメモリの仕組みを学びます。
メモリの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]; // ヒープに配列データ
}
StringやVecは実行時にサイズが変わる可能性があるので、ヒープに置かれます。
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型がヒープにデータを置く理由として正しいものはどれですか?
Stringはpush_strなどで後から長さが変わる可能性があります。そのため、柔軟にサイズを変更できるヒープに置かれます。
Q3. 以下のコードで、ヒープにデータが確保される変数はどれですか?let a: i32 = 10; let b = String::from("hi"); let c: [i32; 3] = [1,2,3];
a(i32)とc(固定長配列)はスタックのみに置かれます。b(String)はスタックにポインタ・長さ・容量を持ち、実際の文字データはヒープに置かれます。
Q4. 以下のコードがコンパイルエラーになる理由は何ですか?let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);
Stringはヒープにデータを持つため、代入時にコピーではなくムーブが発生します。s1の所有権がs2に移動した後、s1は無効となり使用できません。s1.clone()を使えば明示的にコピーできます。
Q5. let x = 5; let y = x;の実行後、xとyの状態として正しいものはどれですか?
i32はCopy型なので、代入時にスタック上で値がコピーされます。xとyはそれぞれ別々のメモリ位置に同じ値5を持ち、両方とも有効です。
次のドキュメント: 02_ownership.md