栈(Stack)与堆(Heap)

理解 Rust 所谓的所有权(ownership)之前,首先要弄清楚这两块内存区域。如果你学过 C 或 C++,你会发现这些概念几乎是一样的。

栈内存

在函数里直接声明一个变量时,它通常位于栈上。例如:

fn main() {
    let a = 1;
    let y = plus_one(a);
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

上面代码中的 axy 都在栈上自动申请和释放,就像 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);
}

编译时会报错,提示 s1s2 被移动了。原因是 StringVecBox 等类型没有实现 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 所有权的进阶概念后,会继续更新。