关注「Rust编程指北」,一起学习 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 */
};
这些是原始指针。一般来说,最好别使用它们,因为只有 unsafe 代码才能进行解引用,而 Rust 的重点是编写尽可能多的安全代码。
原始指针就像 C 中的指针。如果你创建一个指针,你最终会使用 sizeof(struct T *)
字节作为指针。即:
struct T *ptr;
这些是借用引用。它们使用与原始指针相同的地址空间,并在生成的机器代码中以完全相同的方式运行。考虑这个简单的例子:
#[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
请注意,这两个函数是逐位相同的。
借用引用和原始指针之间的唯一区别是:
(第三点随着时间的推移会变得更好。)
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(T { ... });
大致相当于:
struct box_of_t x;
x.heap_ptr = malloc(sizeof(struct T));
if (!x.heap_ptr)
oom();
*x.heap_ptr = ...;
这是借用切片,这是事情变得有趣的地方。尽管看起来它们只是引用(如前所述,转换为简单的 C 样式指针),但它们远不止于此。这些类型的引用使用 胖指针 —— 即指针和长度的组合。
struct fat_pointer_to_T {
struct T *ptr;
size_t nelem;
};
这非常强大,因为它允许在运行时进行边界检查并且获取切片的子集基本上是无损耗的!
这些是对数组的借用引用。它们与借用切片不同。由于数组的长度是编译时常量(如果 n 不是常量,编译器报错),所有边界检查都可以静态执行。因此不需要在胖指针中传递长度。所以它们作为普通指针传递。
struct T *ptr;
虽然这些不是指针,但为了完整起见,我把它们包括在这里。
就像在 C 中一样,结构使用其类型所需的尽可能多的空间(即,其成员的大小加上填充的总和)。
就像在 C 中一样,结构体数组使用结构体大小的 n 倍。
注意,你不能构造 [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];
};
表格如下:
Rust | C | Size on ILP32/LP64 (bytes) | |
---|---|---|---|
Value | let x: T; | struct T x; | 100/100 |
Raw pointer | let x: *const T; let x: *mut T; | struct T *x; | 4/8 |
Reference | let x: &T; let x: &mut T; | struct T *x; | 4/8 |
Box | let x: Box<T>; | struct box_of_T { struct T *heap_ptr; }; struct box_of_T x; | 4/8 |
Array of 2 | let x: [T; 2]; | struct T x[2]; | 200/200 |
Reference to an array of 2 | let x: &[T; 2]; | struct T *x; | 4/8 |
A slice | let x: [T]; | struct T x[]; | unknown at compile time |
A reference to a slice | let x: &[T]; | struct fat_ptr_to_T { struct T *ptr; size_t nelem; }; struct fat_ptr_to_T x; | 8/16 |
提醒一句:我假设各种指针的大小实际上是实现细节,不应该依赖于这种方式。
我没有介绍 str
、&str
、String
和 Vec<T>
,因为我不认为它们是基本类型,而是构建在切片、结构、引用和 Box 之上的便利类型。
作者:JeffPC,原文链接:https://blahg.josefsipek.net/?p=580
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
推荐阅读
觉得不错,点个赞吧
扫码关注「Rust编程指北」