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

指標

本集目標

理解記憶體位址的概念,知道指標在底層是什麼東西。

概念說明

前面幾章我們用 &TBox<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
}

&TBox<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]&strBox<[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 位元系統上的大部分情況下,&TBox<T> 的大小是 8 bytes,就是一個位址的大小
  • * 解參考,透過位址取得對應的內容,這有一層間接存取的成本
  • &[T]&str 是胖指標(fat pointer),佔 16 bytes(位址 + 長度),因為 DST 的大小不固定