Rust 是一门系统编程语言,因此必定能够以原始方式访问内存。Rust 和 C++ 一样,通过指针进行这一操作。指针是一个 Rust 和 C++ 在语法和语义上差异巨大的语言特性。Rust 通过对指针进行类型检查保证内存的安全性。这是 Rust 相较其他语言的主要优势之一。虽然类型系统有点复杂,但用户可以受益于内存安全和底层硬件的性能。
我之前打算一篇文章把 Rust 的指针讲完,但又觉得这一主题太广了。因此,这篇文章只讲独占指针这一种,剩下的后面再讲。
首先是没有指针的例子:
fn foo() {
let x = 75;
// 对 `x` 进行操作
}
到达 foo
的结尾,x
离开作用域(Rust 这一点和 C++ 类似)。这意味着程序无法再访问变量,而变量所占的内存可以重用。
在 Rust 中,对于任意类型 T
,可以写 Box<T>
以得到一个指向 T
的独占指针(owning pointer, unique pointer)。使用 Box:new(...)
在堆上分配内存,并用提供的值初始化此内存。此操作与 C++ 的 new
相似。例如:
fn foo() {
let x = Box::new(75);
}
此处的 x
是一个指向堆的指针,对应的堆内存包含的值为 75
。x
的类型为 Box<i32>
;我们也可以写成 let x: Box<i32> = Box::new(75);
。这与 C++ 的 int* x = new int(75);
类似。与 C++ 不同的是,Rust 会为我们回收内存,因此无需调用 free
或 delete
1。独占指针的表现和值类似,当变量离开作用域时销毁。上例中,foo
函数结束处,无法访问 x
,x
指向的内存可以重用。
与 C++ 相同,使用 *
解引用独占指针。例如:
fn foo() {
let x = Box::new(75);
println!("`x` points to {}", *x);
}
和 Rust 的原始类型一样,独占指针及其指向的数据默认是不可变的。与 C++ 不同的是,无法创造出指向不可变数据的可变指针(译者注:即 const T*
),反过来(译者注:即 T* const
)也不行。数据的可变性与指针一致。例如:
fn foo() {
let x = Box::new(75);
let y = Box::new(42);
// x = y; // 不允许,x 不可变
// *x = 43; // 不允许,*x 不可变
let mut x = Box::new(75);
x = y; // 允许,x 可变
*x = 43; // 允许,*x 可变
}
(译者注:前面的 x
只是被覆盖了,但指向的内存没有被回收。)
独占指针可从函数返回,生存期得以延续。如果独占指针被返回,则内存不会被回收,换言之,Rust 中不会出现悬垂指针。内存不会泄漏。然而,指针最终还是会离开作用域,使得内存被回收。例如:
fn foo() -> Box<i32> {
let x = Box::new(75);
x
}
fn bar() {
let y = foo();
// 使用 y
}
上例中,foo
初始化内存,并返回给 bar
。x
从 foo
中返回并存至 y
,因此没有被销毁。bar
结束时,y
离开作用域,内存被回收。
独占指针是线性(linear)的,因为一块内存只能同时由一个独占指针指向。这一机制通过移动语义实现。当指针指向值时,先前的任何指针都无法访问。例如:
fn foo() {
let x = Box::new(75);
let y = x;
// x 无法访问
// let z = *x; // 错误
}
类似的是,如果独占指针传给了另一个函数,或存储至字段(field)中,此独占指针无法访问:
fn bar(y: Box<isize>) {
}
fn foo() {
let x = Box::new(75);
bar(x);
// x 无法访问
// let z = *x; // 错误
}
Rust 的独占指针和 C++ 的 std::unique_ptr
类似。和 C++ 一样,在 Rust 中,同一个值只能由一个独占指针指向,当指针离开作用域时,指向的值就被销毁。Rust 的检查大多静态进行,而非运行时进行。在 C++ 中访问值被移走的独占指针会导致运行时错误(因为指针为空)。Rust 中,此操作会产生编译错误,因此运行时不会出错。(译者注:关于此处的编译错误,请见 1 中关于移动的说明。)
之后我们会看到,Rust 中可以创建其他类型的指向独占指针的值的指针类型。这一点和 C++ 类似。然而 C++ 中,这么做允许持有指向已回收内存的指针,导致运行时错误。Rust 中不会出现(后面讲 Rust 其他的指针类型时会提到)。
从上例可见,要使用独占指针指向的值,必须进行解引用。然而,方法调用会自动解引用,因此调用方法无需 ->
运算符或使用 *
。这使得 Rust 的指针既有些像 C++ 的指针,又有些像引用。例如:
fn bar(x: Box<Foo>, y: Box<Box<Box<Box<Foo>>>>) {
x.foo();
y.foo();
}
假设类型 Foo
带有方法 foo()
,则上面两个表达式都是正确的。
用已有值调用 Box::new()
不会引用已有值,而是复制已有值,因此:
fn foo() {
let x = 3;
let mut y = Box::new(x);
*y = 45;
println!("x is still {}", x);
}
总的来说,Rust 中带有移动语义,而非复制语义(上面独占指针的例子可见)。原始类型带有复制语义,因此上例中的值 3
会被复制,而更复杂的类型会被移动。之后会详细讲。
然而,有时在编程时,我们需要对值进行多次引用。Rust 中用借用指针实现这一用途。下篇文章讲这个。
Footnotes
-
C++11 中引入的
std::unique_ptr<T>
一定程度上和 Rust 的Box<T>
类似,不过二者有着明显的不同。相同点:
- 由 C++11 的
std::unique_ptr<T>
和 Rust 的Box<T>
指向的内存会在自身离开作用域时自动回收。 - C++11 的
std::unique_ptr<T>
和 Rust 的Box<T>
都只有移动语义。
不同点:
- C++11 允许
std::unique_ptr<T>
从已有的指针构造,从而允许多个独占指针指向同一块内存。Box<T>
不允许此行为。 - C++11 中,对移动至另一变量或函数的
std::unique_ptr<T>
进行解引用导致未定义行为。在 Rust 中,类似行为无法通过编译。(译者注:Rust 的移动语义和 C++ 的移动语义不太一样。Rust 中,对象被移走就不能再用,尝试使用会出现编译错误。C++ 中,对象被移走后仍然可以使用,例如,std::unique_ptr<T>
被移走后,自身存储的指针为空指针;如果移动操作实现不当,或用户使用不当,就可能会出现运行错误。) - 可变性或不可变性无法“穿透”
std::unique_ptr<T>
:解引用const std::unique_ptr<T>
返回的仍然是底层数据的可变(非const
)引用。Rust 中,不可变的Box<T>
不允许修改指向的数据。(译者注:C++ 的标准库扩展 v2 中提供了std::experimental::propagate_const
。此设施实现了“穿透”行为,适合用std::unique_ptr<T>
实现 pImpl 编程技巧的场合。)
Rust 中的
let x = Box::new(75)
可解释为 C++11 中的const auto x = std::unique_ptr<const int>{new int{75}};
以及 C++14 中的const auto x = std::make_unique<const int>(75);
。 ↩ ↩2 - 由 C++11 的