stack 與 heap
本集目標
理解 stack 和 heap 的差別,並揭秘第 1 集鑰匙圈比喻的真正含義。
概念說明
記憶體中兩個常見的位置
程式執行時,資料會被放在記憶體裡。這一集先來認識兩個常見的存放位置:stack 和 heap。
堆疊(stack):
- 函數被呼叫時,通常會用 stack 存放大小在編譯時期就已知的區域變數。
- 到目前為止學過的整數、浮點數、布林值、
char、長度固定的陣列、tuple,以及只包含這些資料的struct,在作為區域變數時通常都直接放在 stack 上。 - 大小在編譯時期就已知的資料不一定很小;重點是編譯器事先知道需要保留多少空間。
- 函數結束時,這次呼叫使用的 stack 空間會一起收回。
堆積(heap):
- 程式執行時,可以視需要另外取得 heap 空間。
- 當資料量在執行時期才知道,或可能隨著程式執行而增長時,資料通常會另外放在 heap 上。例如,如果程式需要存下使用者輸入的所有數字,事前可能不知道總共有幾個。
- 程式會記住之後要到哪裡找到存放在那裡的資料。
- Rust 的所有權系統會決定何時可以歸還這塊空間。
鑰匙圈比喻揭秘!
還記得第 1 集的鑰匙圈比喻嗎?現在來揭秘它真正的意思:
- 鑰匙 = 讓程式之後能找到保險箱的資訊。
- 保險箱 = 另外存放在 heap 上的資料。
- 鑰匙圈上的裝飾 = 直接帶在鑰匙圈上的資料。
所以當我們說「move 是把鑰匙圈交出去」:
- 鑰匙和裝飾會一起交給新的擁有者。
- 保險箱本身留在原處,不需要搬動或重新建立。
為什麼整數是 Copy?
整數(i32 等)就像鑰匙圈上的裝飾。複製整數是單純、機械化的操作,所以整數實作了 Copy。
有些型別還負責管理另外存放在 heap 上的資料,因此不能用同樣的方式自動複製。這類值在賦值時會 move;需要建立副本時,則要明確呼叫 .clone()。接下來幾集會看到具體的例子。
範例程式碼
#[derive(Debug, Copy, Clone)]
struct StackData {
x: i32,
y: i32,
active: bool,
}
fn main() {
// 這些區域變數的大小在編譯時期就已知,
// 所以這些值可以直接放在 stack 上
let a = 42; // i32,4 bytes
let b = 3.14; // f64,8 bytes
let c = true; // bool,1 byte
let ch = '🦀'; // char,4 bytes
println!("整數:{},浮點:{},布林:{},字元:{}", a, b, c, ch);
// struct 直接存放所有欄位,所以也可以直接放在 stack 上
let data = StackData { x: 10, y: 20, active: true };
let data2 = data; // Copy!data 還能用
println!("data = {:?}", data);
println!("data2 = {:?}", data2);
// 長度固定的陣列也可以直接放在 stack 上
let arr = [1, 2, 3, 4, 5];
println!("陣列:{:?}", arr);
// tuple 也可以直接放在 stack 上
let t = (42, true, 'A');
println!("tuple:{:?}", t);
}
重點整理
- stack(堆疊):通常直接存放大小在編譯時期就已知的區域變數,例如整數、固定長度的陣列、tuple,以及由這些資料組成的
struct;函數結束時,這次呼叫使用的空間會一起收回。 - heap(堆積):通常用來存放資料量在執行時期才知道或可能增長的資料;程式會記住之後要到哪裡找到存放在那裡的資料。
- 鑰匙圈比喻揭秘:鑰匙 = 用來找到資料的資訊、保險箱 = 另外存放在 heap 上的資料、裝飾 = 直接帶著的資料。
- 整數的複製是單純、機械化的操作,所以整數實作了
Copy。 - 負責管理另外存放在 heap 上資料的型別,在賦值時會 move;需要建立副本時要明確呼叫
.clone()。