所有权延申
本章是对所有权一章的深入补充,侧重于从底层机制的角度回答以下三个问题:
- 谁可以拥有所有权?
- 编译器如何在底层实现所有权的唯一性保证?
- 借用规则(引用互斥)的根本原因是什么?
一、谁可以拥有所有权
在 Rust 中,有三类实体可以充当"所有者":
1. 变量
这是最常见的形态。当你声明 let s = String::from("hello"); 时,变量 s 就直接拥有了该字符串值的所有权。
2. 数据结构
结构体(struct)、枚举(enum)或容器(如 Vec、HashMap)拥有其内部成员的所有权。例如,一个结构体字段如果是 String 类型,那么该结构体实例就拥有这个字符串,当结构体被销毁时,该字段也会随之被释放。
struct User {
name: String, // User 实例拥有 name 字段的所有权
}3. 函数参数
当值作为参数传递给函数(非引用传递)时,函数的作用域会接管该值的所有权,函数返回后若未将所有权传出,该值即被释放。
fn greet(s: String) { // s 获得传入值的所有权
println!("hello, {}", s);
} // s 离开作用域,值被释放二、所有权的底层机制
所有权不是运行时的某种实体,而是编译器在编译阶段对内存管理责任的静态追踪。
编译器账本
可以把编译器想象成一个记账员,它维护着一个"内存账本":
- 登记: 声明变量时,编译器记录"这块内存地址归此变量管理"
- 转移: 执行
let b = a;时,编译器将账本中a对应的条目划转给b,同时将a标记为无效 - 注销: 作用域结束时(
}),编译器自动在此处插入drop指令,由最后的合法所有者负责清理
通过这套静态账本,编译器在不增加任何运行时开销的情况下,保证了"任意时刻只有一个所有者"这一不变量。
赋值即移动
在其他语言中,赋值通常是复制指针(导致两个变量指向同一块内存)。Rust 的 Move 语义与此不同:
- 执行
let b = a;时,确实发生了栈上数据的浅拷贝(复制指针、长度、容量等字段给b) - 关键动作: 编译器在账本上将
a标记为无效,b成为唯一的合法所有者 - 堆上的真实数据完全没有移动
这种"废除前任,只留现任"的方式,确保了所有者的唯一性,也彻底避免了二次释放(double free)问题。
三、借用互斥的底层根因
所有权一章讲解了借用规则:
- 要么有任意数量的不可变引用
- 要么有唯一一个可变引用
但为什么要有这个限制?真正的根因是内存重分配(Reallocation)。
内存重分配
当 String 或 Vec 在堆上增长且当前空间不足时,Rust 会:
- 在堆上申请一块更大的新内存
- 将旧内存的数据完整搬移到新地址
- 更新栈上结构体(句柄)中的指针字段,指向新地址
- 释放旧内存
关键后果: 搬家后,所有持有旧地址的指针都会瞬间变成野指针(Dangling Pointer)。
可变引用与不可变引用互斥
假设允许以下情况同时存在:
let mut s = String::from("hello");
let r1 = &s; // 不可变引用,持有旧地址 0xA000
let r2 = &mut s; // 可变引用,拥有修改权
r2.push_str("world"); // 触发扩容,String 搬到新地址 0xB000
println!("{}", r1); // ❌ r1 仍指向旧地址 0xA000,访问已释放内存r2扩容后,s的栈上指针已更新为0xB000- 但
r1仍然持有旧地址0xA000,读取时会访问已释放的内存,造成崩溃或安全漏洞
结论: 不可变引用要求数据"原地不动",而可变引用拥有"让数据搬家"的权力,两者在物理层面无法并存。
多个可变引用互斥
let mut s = String::from("hello");
let r1 = &mut s; // r1 持有旧地址 0xA000
let r2 = &mut s; // r2 也持有旧地址 0xA000
r1.push_str("!"); // r1 触发扩容,s 搬到 0xB000,r2 的地址立即失效
r2.push_str("?"); // ❌ r2 仍指向 0xA000,野指针访问即使不发生搬家,两个入口同时写入同一块内存也会导致数据竞争(Data Race),产生不可预测的逻辑错误。
结论: 同一时间只允许一个"修改入口",保证修改操作对内存状态的完全控制。
编译器的执行流程
当你借出一个 &mut 时,编译器在背后执行以下逻辑:
- 锁定: 原变量(栈上的指针、长度、容量)进入"被独占借用"状态,禁止其他引用访问
- 独占修改: 可变引用是唯一合法的访问入口,修改完成后直接回写原变量的栈上字段
- 解锁: 可变引用的生命周期结束后,原变量重新解锁,外界拿到的永远是最新的有效地址
这整套流程全部在编译阶段完成,零运行时开销。
总结
| 角度 | 核心机制 |
|---|---|
| 所有者类型 | 变量、数据结构字段、函数参数均可持有所有权 |
| 底层实现 | 编译器维护静态账本,Move = 浅拷贝 + 账本转让 |
| 借用互斥 | 根因是防止内存重分配后的野指针,而非抽象规则 |
本质认识: 所有权和借用规则是编译器的"静态逻辑检查",与 TypeScript 静态类型检查在理念上类似——用编译时的约束换取运行时的安全,且没有额外的性能代价。