栈(Stack)与堆(Heap)
理解 Rust 所谓的所有权(ownership)之前,首先要弄清楚栈与堆这两块内存区域。如果你学过 C 或 C++,你会发现这些概念几乎是一样的。
栈内存
在函数里直接声明一个变量时,它通常位于栈上。例如:
fn main() {
let a = 1;
let y = plus_one(a);
}
fn plus_one(x: i32) -> i32 {
x + 1
}
上面代码中的 a
、x
与 y
都在栈上自动申请和释放,就像 C/C++ 那样。
堆内存
在栈的使用上,Rust 与 C/C++ 完全一致;然而堆却截然不同。Rust 编译器接管了堆内存的管理,通过一系列约束来避免常见的错误或未定义行为(UB, Undefined Behavior)。
Rust 提供了 Box
这一结构体,用来在堆上分配数据:
let a = Box::new([0; 1_000_000]);
这里创建了一个位于堆上的数组,而指针 a
本身仍位于栈上。
如前所述,Rust 会自动释放堆内存。那么何时释放 Box
指向的堆内存呢?直观地说,当再也没有指针指向这块内存时,就可以回收——这与某些语言(如 Java、C#)的垃圾回收思路类似。但 Rust 采用了基于作用域(scope)的不同机制:
如果一个变量拥有(own) 一个
Box
,当该变量的栈内存被释放时,对应的堆内存也会被释放。
这就引出了“所有权”一词,不过在此之前先看看引用。
引用(References)
假设我们要写一个函数,接收两条字符串并在一行中打印。最直观的写法如下:
fn main() {
let s1 = String::from("Hello");
let s2 = String::from("world");
greet(s1, s2);
println!("{} {}!", s1, s2);
}
fn greet(t1: String, t2: String) {
println!("{} {}!", t1, t2);
}
编译时会报错,提示 s1
与 s2
被移动了。原因是 String
、Vec
、Box
等类型没有实现 Copy
trait。对于实现了 Copy
的类型(如 i32
),类似代码不会出错,因为函数只是复制了它们的值。若不想移动变量,就需要用引用。
与 C/C++ 类似,Rust 用 &
表示引用,用 *
表示解引用:
fn main() {
let s1 = String::from("Hello");
let s2 = String::from("world");
greet(&s1, &s2);
println!("{} {}!", s1, s2);
let mut n: Box<i32> = Box::new(1);
let a = *n;
*n += 1;
println!("a = {a}, n = {n}"); // 输出: a = 1, n = 2
}
fn greet(t1: &String, t2: &String) {
println!("{} {}!", t1, t2);
}
上述代码展示了引用与解引用的基本用法。
但是……
引用功能强大,却可能带来问题(写过 C/C++ 指针的人深有体会)。例如,执行下面代码会产生未定义行为:
let mut v: Vec<i32> = vec![1, 2, 3];
let n: &i32 = &v[2];
v.push(4);
println!("n = {n}"); // UB:指针使用了已被释放的内存
push
触发 v
重新分配更大的数组,导致原有数组被释放,n
指向了被释放的内存。
为避免此类问题,Rust 引入了**借用检查器(borrow checker)**来施加约束。
借用检查器(Borrow Checker)
借用检查器的核心思想:每个变量对其数据拥有三种“权限”:
- 读(Read):可复制数据到别处。
- 写(Write):可修改数据。
- 拥有(Own):可移动或释放数据。
这些权限描述的是编译器在代码执行前的静态观点,而非运行时行为。
- 默认情况下,变量拥有读与拥有权限。
- 若变量声明时带
mut
,则再获写权限。
关键点:权限是会变化的。看下例:
let mut v: Vec<i32> = vec![1, 2, 3]; // [v: R W O]
let n: &i32 = &v[2]; // [v: R - -] [n: R - O] [*n: R - -]
println!("n = {n}"); // [v: R W O] [n: - - -] [*n: - - -]
v.push(4); // [v: - - -] [n: - - -] [*n: - - -]
当 n
借用了 v
的数据:
v
失去写与拥有权限,仅剩读。n
获得读与拥有权限(它本身不可变,因此无写)。*n
获得读权限。
注意:n
与 *n
权限不同——修改引用本身与通过引用修改数据是两回事。再看例子:
let mut v: Vec<i32> = vec![1, 2, 3]; // [v: R W O]
let mut t: &i32 = &v[0]; // [v: R - -] [t: R W O] [*t: R - -]
t = &v[1]; // [v: R - -] [t: R W O] [*t: R - -]
println!("t = {t}"); // [v: R W O] [t: - - -] [*t: - - -]
let n: &mut i32 = &mut v[2]; // [v: - - -] [n: R - O] [*n: R W -]
*n = 0; // [v: R W O] [n: - - -] [*n: - - -]
println!("{:?}", v); // 输出: [1, 2, 0]
结语
本篇先写到这里。等我学到更多 Rust 所有权的进阶概念后,会继续更新。