指標
本集目標
理解記憶體位址的概念,知道指標在底層是什麼東西。
概念說明
前面幾章我們用 &T、Box<T>、Rc<T> 的時候,關心的是「誰擁有資料」、「誰在借」。這一集要換個角度——這些東西在記憶體裡到底是什麼。
附錄一最後一集曾經提過 DST 和胖指標的概念。如果當時覺得看不太懂是正常的——因為那時候我們還沒正式介紹過指標。這一集就是要把這個基礎補上。
記憶體位址
程式執行的時候,每一個變數都會被放在記憶體的某個位置,而每個位置都有一個編號,叫做位址。&x 拿到的就是 x 的位址。用 {:p} 格式化可以把它印出來看:
fn main() {
let x: i32 = 42;
println!("{:p}", &x); // 例如 0x7ffd5e8a3b4c
}
這個十六進位的數字就是 x 在記憶體中的位址。
&T 的真面目
&x 產生的值,一般來說就是 x 的記憶體位址。&T 這個型別存的東西,本質上也就是作為位址的數字。當你把 &x 傳進函數的時候,傳的不是 x 的內容,是 x 的位址。
指標大小
在大部分情況下,&T 佔 8 bytes,在 64 位元系統上就是一個位址的大小。用 std::mem::size_of 來驗證:
use std::mem::size_of;
fn main() {
println!("{}", size_of::<i32>()); // 4
println!("{}", size_of::<[i32; 1000]>()); // 4000
println!("{}", size_of::<&i32>()); // 8
println!("{}", size_of::<&[i32; 1000]>()); // 8
println!("{}", size_of::<Box<i32>>()); // 8
}
&T 和 Box<T> 大小一樣——因為它們存的都是位址。&T 指向的資料可能在 stack 上也可能在 heap 上,而擁有所有權的 Box<T> 指向的一定在 heap 上。不管指向哪裡,位址本身的大小是一樣的。所以當 T 本身很大的時候,傳一個位址會比複製整個 T 輕量——但代價是每次存取都要透過位址去查找,多了一層間接。
解參考
有了位址,我們能做什麼?用 * 運算子解參考(dereference),透過位址取得對應的內容:
fn main() {
let x = 42;
let r = &x;
println!("{}", *r); // 透過位址取得值:42
}
解參考不是免費的,大部分時候這個成本很小,但知道它的存在是有意義的。
胖指標(fat pointer)
附錄一最後一集介紹過 DST——[T] 和 str 是大小不確定的型別,沒辦法直接放在變數裡,通常要透過 &[T]、&str、Box<[T]> 等方式使用。但 DST 的大小不固定,光有位址不夠。想像一下:你拿到一個位址,知道從這裡開始是一段連續的 i32 資料——但到哪裡結束?記憶體本身不會告訴你,位址只是一個起點。所以除了位址之外,還得額外記錄長度,才知道這段資料有多長。因此 &[T] 和 &str 佔 16 bytes:
use std::mem::size_of;
fn main() {
println!("{}", size_of::<&i32>()); // 8(位址)
println!("{}", size_of::<&[i32]>()); // 16(位址 + 長度)
println!("{}", size_of::<&str>()); // 16(位址 + 長度)
}
範例程式碼
use std::mem::size_of;
fn main() {
let x: i32 = 42;
let r: &i32 = &x;
// 印出位址
println!("x 的位址:{:p}", &x);
println!("r 存的值:{:p}", r); // 和上面一樣
// 解參考
println!("透過 r 取得 x 的值:{}", *r);
// 智慧指標也能解參考
let b = Box::new(99);
println!("Box 裡的值:{}", *b);
// 指標大小
println!("--- 一般指標 ---");
println!("i32 大小:{} bytes", size_of::<i32>());
println!("&i32 大小:{} bytes", size_of::<&i32>());
println!("[i32; 1000] 大小:{} bytes", size_of::<[i32; 1000]>());
println!("&[i32; 1000] 大小:{} bytes", size_of::<&[i32; 1000]>());
println!("Box<i32> 大小:{} bytes", size_of::<Box<i32>>());
// 胖指標
println!("--- 胖指標 ---");
println!("&[i32] 大小:{} bytes", size_of::<&[i32]>());
println!("&str 大小:{} bytes", size_of::<&str>());
println!("Box<[i32]> 大小:{} bytes", size_of::<Box<[i32]>>());
}
重點整理
&T在底層就是一個記憶體位址,本質上是一個數字- 在 64 位元系統上的大部分情況下,
&T和Box<T>的大小是 8 bytes,就是一個位址的大小 - 用
*解參考,透過位址取得對應的內容,這有一層間接存取的成本 &[T]和&str是胖指標(fat pointer),佔 16 bytes(位址 + 長度),因為 DST 的大小不固定