Rust 所有权与借用深入
更新时间:2026-08-29。本文回答:Rust 没有 GC 怎么保证内存安全?移动语义到底是什么?借用规则为什么能消灭数据竞争?
一、为什么需要所有权
C/C++ 里内存错误的根源,几乎都出在"谁负责释放、什么时候释放"上:悬垂指针(释放后还用)、二次释放(释放两次)、泄漏(忘了释放)。Rust 的解法是把"释放责任"变成一条编译期规则:每个值有且仅有一个所有者(owner),所有者离开作用域,值就被自动释放。
| 语言 | 释放时机 | 典型 bug |
|---|---|---|
| C | 手动 free | 悬垂、二次释放、泄漏 |
| C++ | RAII / 智能指针 | 循环引用、裸指针误用 |
| Java/Go | GC 标记清除 | GC 停顿、STW |
| Rust | 所有者离开作用域即 drop | 编译期直接拒绝上述 bug |
核心结论:Rust 把"内存管理正确性"从"程序员小心"变成了"编译器强制"。
二、栈与堆,以及移动
rust
let s1 = String::from("hello"); // 堆上分配
let s2 = s1; // 所有权移动:s1 失效
// println!("{}", s1); // 编译错误String是拥有堆数据的类型,移动时只转移"所有权",不深拷贝堆内容(性能友好)。- 像
i32、bool这类大小固定的简单类型实现了Copytrait,let a = b;是按位复制而非移动,原变量仍可用。
rust
let x = 5;
let y = x; // Copy:x 仍然是 5三、借用规则:多读或一写
引用(&T / &mut T)让你在不转移所有权的前提下使用值。编译器靠两条规则在编译期杜绝数据竞争:
- 要么有任意多个不可变借用(
&T); - 要么有恰好一个可变借用(
&mut T); - 二者不能同时存在。
rust
fn main() {
let mut v = vec![1, 2, 3];
let r1 = &v; // 不可变借用
let r2 = &v; // OK:多个只读
println!("{} {}", r1[0], r2[0]);
let w = &mut v; // 必须等 r1/r2 不再使用后
w.push(4); // 唯一的可写者
println!("{}", w.len());
}为什么这能消灭数据竞争?因为"写入时存在并发读取"正是数据竞争的定义,而借用检查器从根上禁止了"同一作用域内又读又写共存"。
四、与本站主线的衔接
| Rust 概念 | 对应本站知识 | 衔接文档 |
|---|---|---|
堆分配 Box/String | L3 内存子系统 | 理解分配热点、cache miss |
| drop / RAII | crash 排查线 | C++ 智能指针同源 |
| 借用检查 | L4 内存序与原子 | 编译期替代运行期锁 |
| 移动 vs Copy | L5 内存布局 | 值语义与对象布局 |
实际排查 Rust 程序的内存/CPU 问题,工具链仍是本站的 perf、valgrind 类比(Rust 用 cargo build + perf record 同样能出火焰图)。
五、常见坑
- 试图返回局部引用:
fn bad() -> &String编译失败,因为返回值引用了即将销毁的局部变量——这正是生命周期检查在保护你。 - 在迭代中同时借可变与不可变:
for x in &v { v.push(...); }报错,迭代器持有不可变借用期间不能再可变借用v。 Rc/RefCell打破单所有者:单线程共享用Rc<T>,内部可变性用RefCell<T>;多线程则换Arc+Mutex。
一句话总结
Rust 的所有权 = 编译期排布每一块内存的释放点;借用规则 = 编译期禁止"又读又写"的数据竞争——没有 GC,也能既安全又快。
继续:生命周期、trait 与泛型 → 错误处理与异步。