Rust 中指針那些事

大家好,我是胖蟹哥。

現在高級語言很多都沒有指針,可能因爲指針太靈活、太難。Rust 支持指針,那和 C 比較有什麼異同呢?本文聊聊這個問題。


我已經關注 Rust[1] 大約一年了。於是決定用它來製作一個簡單的小程序,或者在其中實現一些簡單的功能,以親眼看看它到底有多符合人體工程學,以及 rustc 編譯出什麼樣的機器代碼。但是上週末我發現需要一個工具來清理一些預處理器的問題,所以我決定用 Rust 編寫它,而不是將 Shell 和 Python 組合在一起。

從我之前經驗看,我知道有很多不同的 “指針”,但我發現對它們的所有描述很少或者不明確。具體來說,Rust 稱自己是一種系統編程語言,但我沒有找到關於不同指針如何映射到 C(系統編程語言)的明確描述。最終,我偶然發現了 The Periodic Table of Rust Types[2],這讓事情變得更清晰了一些,但我仍然覺得沒有真正理解。

週末,我對 Rust 進行了各種探索,已經掌握了足夠的東西來寫這篇關於 Rust 如何做事的解釋。歡迎反饋。

我將描述 C 中對應的術語。爲了簡單起見,我將:

下文中,我們假設存在 struct T,具體字段無關緊要,即:

struct T {
 /* some members */
};

const T 和 mut T

這些是原始指針。一般來說,最好別使用它們,因爲只有 unsafe 代碼才能進行解引用,而 Rust 的重點是編寫儘可能多的_安全_代碼。

原始指針就像 C 中的指針。如果你創建一個指針,你最終會使用 sizeof(struct T *) 字節作爲指針。即:

struct T *ptr;

&T and &mut T

這些是借用引用。它們使用與原始指針相同的地址空間,並在生成的機器代碼中以完全相同的方式運行。考慮這個簡單的例子:

#[no_mangle]
pub fn raw(p: *mut usize) {
    unsafe {
        *p = 5;
    }
}

#[no_mangle]
pub fn safe(p: &mut usize) {
    *p = 5;
}

經過 rustc 編譯後:

raw()
    raw:     55                 pushq  %rbp
    raw+0x1: 48 89 e5           movq   %rsp,%rbp
    raw+0x4: 48 c7 07 05 00 00  movq   $0x5,(%rdi)
             00 
    raw+0xb: 5d                 popq   %rbp
    raw+0xc: c3                 ret    

safe()
    safe:     55                 pushq  %rbp
    safe+0x1: 48 89 e5           movq   %rsp,%rbp
    safe+0x4: 48 c7 07 05 00 00  movq   $0x5,(%rdi)
              00 
    safe+0xb: 5d                 popq   %rbp
    safe+0xc: c3                 ret

請注意,這兩個函數是逐位相同的。

借用引用和原始指針之間的唯一區別是:

  1. 引用永遠不會指向虛假地址(即,它們永遠不會爲 NULL 或未初始化),

  2. 編譯器不允許你對引用進行任意指針運算,

  3. 借用檢查器會讓你在一段時間內質疑你的生活。。。Rust 的難點之一

(第三點隨着時間的推移會變得更好。)

Box<T>

即智能指針。如果你是 C++ 程序員,那麼你大概率已經熟悉它們了。

幾乎所有的文檔和教程都說,Box<T> 不是指針,而是一個包含指向堆分配內存的結構,該內存大到足以容納 T。堆分配和釋放是自動處理的。(分配是在 Box::new 函數中完成的,而釋放是通過 Drop trait[3] 完成的,但這與內存佈局無關。)換句話說,Box<T> 類似於:

struct box_of_T {
 struct T *heap_ptr;
};

Then, when you make a new box you end up putting only what amounts to sizeof(struct T *) on the stack and it magically starts pointing to somewhere on the heap. In other words, the Rust code like this:

然後,當你創建一個新的 box 時,你最終只會把 sizeof(struct T *) 放在堆棧上,它神奇地開始指向堆上的某個地方。換句話說,Rust 代碼是這樣的:

let x = Box::new({ ... });

大致相當於:

struct box_of_t x;

x.heap_ptr = malloc(sizeof(struct T));
if (!x.heap_ptr)
 oom();

*x.heap_ptr = ...;

&[T] 和 &mut [T]

這是借用切片,這是事情變得有趣的地方。儘管看起來它們只是引用(如前所述,轉換爲簡單的 C 樣式指針),但它們遠不止於此。這些類型的引用使用 胖指針 —— 即指針和長度的組合。

struct fat_pointer_to_T {
 struct T *ptr;
 size_t nelem;
};

這非常強大,因爲它允許在運行時進行邊界檢查並且獲取切片的子集基本上是無損耗的!

&[T; n] 和 &mut [T; n]

這些是對數組的借用引用。它們與借用切片不同。由於數組的長度是編譯時常量(如果 n 不是常量,編譯器報錯),所有邊界檢查都可以靜態執行。因此不需要在胖指針中傳遞長度。所以它們作爲普通指針傳遞。

struct T *ptr;

T, [T; n] 和  [T]

雖然這些不是指針,但爲了完整起見,我把它們包括在這裏。

T

就像在 C 中一樣,結構使用其類型所需的儘可能多的空間(即,其成員的大小加上填充的總和)。

[T; n]

就像在 C 中一樣,結構體數組使用結構體大小的 n 倍。

[T]

注意,你不能構造 [T]。當你考慮該類型的含義時,這實際上是完全合理的。這就是說我們有一些_可變大小_的內存切片,以便對 T 類型元素進行訪問。由於這是可變大小的,編譯器不可能在編譯時爲其保留空間,因此我們會收到編譯器錯誤。

更復雜的答案涉及 Sized trait[4],到目前爲止我已經巧妙地設法避免了它。

總結

That was a lot of text, so I decided to compact it and make the following table. In the table, I assume that our T struct is 100 bytes in size. In other words:

以上內容不少,爲了方便,我把它製作成下表。在表中,我假設 T 結構的大小爲 100 字節:

/* Rust */
struct T {
    stuff: [u8; 100],
}

/* C */
struct T {
 uint8_t stuff[100];
};

表格如下:

aTuDRs

提醒一句:我假設各種指針的大小實際上是實現細節,不應該依賴於這種方式。

我沒有介紹 str&strStringVec<T> ,因爲我不認爲它們是基本類型,而是構建在切片、結構、引用和 Box 之上的便利類型。

作者:JeffPC,原文鏈接:https://blahg.josefsipek.net/?p=580

[1] Rust: https://www.rust-lang.org/

[2] The Periodic Table of Rust Types: http://cosmic.mearie.org/2014/01/periodic-table-of-rust-types/

[3] Drop trait: https://doc.rust-lang.org/std/ops/trait.Drop.html

[4] Sized trait: https://doc.rust-lang.org/std/marker/trait.Sized.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/B3j-tiq7Zcwn2RAvVrpKwg