所有权、借用与生命周期:无 GC 的内存安全基石
基于进阶语言 · 核于 2026-08
速查
- 所有权三规则:①每个值有唯一所有者;②所有者离开作用域,值被自动释放(
Droptrait,RAII);③赋值/传参默认移动(move)——所有权转交,原变量失效;实现Copytrait 的类型(i32/bool/&T)则栈上拷贝,仍可用。 - move 的本质:
String/Vec/Box这类持有堆内存的类型,move 只拷贝栈上的「指针/长度/容量」三元组,堆内存归属转移——既避免深拷贝开销,又杜绝双重释放。深拷贝要显式clone()。 - 借用(引用):传
&T(不可变,可多个)或&mut T(可变,唯一)。铁律:任意时刻「多个&T」或「一个&mut T」,二者不可共存——这是编译期消灭 aliasing bug 与数据竞争的根本。 - 借用检查器(borrow checker):编译器组件,静态分析每个引用的活跃区间,拒绝违反规则的代码。一旦通过,运行时绝不可能出现悬垂引用/双重释放/数据竞争。
- NLL(Non-Lexical Lifetimes):2018 版引入,引用生命周期到「最后一次使用」而非「作用域括号」,大幅减少无谓的编译错误。
- 生命周期标注
'a:当函数有多个引用参数/返回引用时,用'a描述它们的关系(不改变实际寿命)。编译器据此验证「返回的引用不会比被引用者活得更久」。'static表示活整个程序(字符串字面量)。 - 悬垂引用 = 编译错误:函数不能返回局部变量的引用(局部变量函数结束即释放)。
- 智能指针(堆上分配 + 额外语义):
Box<T>(独占堆,单线程)、Rc<T>(引用计数,单线程共享)、Arc<T>(原子引用计数,多线程共享)、RefCell<T>/Mutex<T>(运行期借用检查/内部可变性)。String/Vec<T>本质也是智能指针。 - 所有权 vs GC vs 手动 free:Rust 所有权 = 编译期 + 确定性(无 STW);Java/Go GC = 运行期 + 非确定(有停顿);C/C++ 手动 = 运行期 + 确定但易错。
一、move 语义:默认是转移不是拷贝
C/C++ 默认赋值是拷贝(深拷贝昂贵,或浅拷贝致双重释放),Rust 反其道——默认 move:
rust
fn main() {
let s1 = String::from("hello");
let s2 = s1; // move:s1 的堆指针/长度/容量拷给 s2,s1 标记失效
// println!("{}", s1); // ❌ value borrowed here after move
let x = 5;
let y = x; // Copy:i32 实现了 Copy trait,栈上拷贝,x 仍可用
println!("{} {}", x, y);
}String 在内存中的布局(move 前):
栈 s1:[ ptr ──┐, len=5, cap=5 ]
│
▼ 堆:[ h e l l o ]
move 之后:
栈 s1:[ 失效 ] 栈 s2:[ ptr ──┐, len=5, cap=5 ]
│
▼ 堆:[ h e l l o ] (同一块,归 s2)- 为什么安全:move 后
s1在编译层面就失效了,再用它编译器直接报错——堆内存始终只有一个所有者,析构时不会双重释放。 Copytrait:栈上简单类型(i32/f64/bool/char/不含堆的元组/数组)实现 Copy,赋值时拷贝而非 move,原变量仍可用。要实现 Copy,类型必须所有字段都是 Copy(不能含String/Box)。- 函数调用同理:传
String给函数会 move(除非用引用),函数结束后该值在函数内释放。
二、借用与引用
每次都 move 太笨重(函数用完值就没了)。借用(borrow)让你不获取所有权地使用值:
rust
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传 &s(不可变引用),s 所有权不变
println!("'{}' 的长度是 {}", s, len); // ✅ s 仍可用
}
fn calculate_length(s: &String) -> usize { // 借用,不拥有
s.len()
} // s 离开作用域,但因为它不拥有,什么都不释放两条借用规则(违反即编译错误):
- 多个不可变借用 OR 一个可变借用,不可同时存在:rust
let mut s = String::from("hi"); let r1 = &s; let r2 = &s; // ✅ 多个 & let r3 = &mut s; // ❌ 已有 &,不能再 &mut - 引用必须始终有效(不可悬垂):rust
fn dangle() -> &String { // ❌ 返回局部变量的引用 let s = String::from("hi"); &s } // s 在此释放,引用悬垂!
- 可变借用要求被借变量
mut:let mut s,且只能有一个&mut。 - 为什么这套规则消灭数据竞争:数据竞争 = 一写一读无同步。借用规则让「写」必须独占——单线程里消灭 aliasing bug,多线程里(配合 Send/Sync)编译期消灭数据竞争。
三、NLL:让借用规则更人性化
2015 版的借用规则按词法作用域判断,导致很多「看起来没问题」的代码报错。NLL(2018 版起默认)改为按最后一次使用点:
rust
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // r1、r2 最后一次使用在此
// —— NLL 认为 r1/r2 在此之后不再活跃 ——
let r3 = &mut s; // ✅ 可变借用(老规则会因 r1/r2 作用域到这而报错)
println!("{}", r3);NLL 不改变安全保证,只是更精确地建模「引用何时真正结束」,让合理的代码能通过。
四、生命周期标注
当函数返回引用且参数有多个引用时,编译器无法自动推断返回值与哪个参数同寿命,需要你标注 'a:
rust
// 'a 表示:返回的引用至少和 x、y 中较短的那个一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long");
let result;
{
let s2 = String::from("xy");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // ✅ s2 还活着
}
// println!("{}", result); // ❌ s2 已释放,result 可能指向 s2 → 编译错误
}'a是描述而非命令:你声明「返回引用与参数同寿命」,编译器据此检查每次调用是否合规。- 结构体里的引用也要标注生命周期:
struct Foo<'a> { part: &'a str }——保证Foo不会比它借用的part活得久。 - 生命周期省略规则:常见情况下编译器自动补全(每个引用参数一个生命周期、若只有一个输入则输出同寿命、
&self/&mut self方法输出同 self 寿命)。
五、智能指针:堆分配与多所有者
Box/Rc/Arc/RefCell 是「类引用但拥有数据」的智能指针,扩展所有权模型:
| 智能指针 | 用途 | 借用检查时机 | 线程安全 | 典型场景 |
|---|---|---|---|---|
Box<T> | 把值放到堆上,独占所有权 | 编译期 | ✅ Send | 递归类型(链表/树)、大对象避免栈拷贝 |
Rc<T> | 引用计数,多所有者(单线程) | 编译期 | ❌ 非 Send | 共享只读数据(图、AST 节点互引) |
Arc<T> | 原子引用计数,多所有者(多线程) | 编译期 | ✅ Send + Sync | 多线程共享不可变数据 |
RefCell<T> | 内部可变性(运行期借用检查) | 运行期(panic 报错) | ❌ | 单线程需要在不可变环境下修改(测试 mock) |
Mutex<T>/RwLock<T> | 内部可变性 + 锁(多线程) | 运行期 | ✅ | 多线程共享可变数据 |
Cow<T> | 写时复制(借或拥有) | 编译期 | 视 B 而定 | 大部分只读、偶尔修改的场景 |
rust
use std::rc::Rc;
fn main() {
// Box:堆上独占
let b = Box::new(5);
println!("{}", b); // 自动解引用(Deref)
// Rc:多所有者(单线程)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // 引用计数 +1,非深拷贝
let c = Rc::clone(&a);
println!("计数 = {}", Rc::strong_count(&a)); // 3
// a/b/c 任何一个最后释放时计数减 1,归零才真正释放
}RcvsArc:Rc计数操作非原子(快但不线程安全),Arc用原子操作(线程安全,多线程必用 Arc)。Rc<T>不能直接改:因为多个所有者,若可变会有数据竞争——要改就用Rc<RefCell<T>>(单线程)或Arc<Mutex<T>>(多线程)。- 循环引用内存泄漏:
Rc互相引用会泄漏,需用Weak<T>弱引用打破环。
六、所有权机制对比其他内存管理
| 机制 | 检查时机 | 回收时机 | 停顿 | 代表 |
|---|---|---|---|---|
| Rust 所有权 | 编译期 | 确定性(作用域结束) | 无 | Rust |
| GC(追踪) | 运行期 | 不确定(GC 触发) | 有 STW | Java、Go、V8 JS |
| GC(引用计数) | 运行期 | 计数归零 | 无(但原子开销) | Swift ARC、Python |
| 手动 free | 无 | 手动 | 无 | C |
| RAII | 无 | 确定性(作用域结束) | 无 | C++(析构)、Rust |
Rust 的所有权是「编译期检查 + 确定性回收」的独特组合——既有 GC 语言的安全(且无停顿),又有 C/C++ 的性能与可控。
下一步
掌握了所有权/借用/生命周期后,下一步看Cargo、trait、泛型与生态——Rust 的工程化工具链、泛型单态化、trait object 动态分发,以及 Rust 在 WASM 与前端基建(SWC/Turbopack)中的崛起。