Rust 的安全感来自一条“死规矩”:谁申请的内存、谁负责把它收回。理解这条规矩离不开两个舞台——栈和堆。下面用三段小代码,但把重点放在“钥匙怎么传、数据在哪里”。
先掰清:栈与堆在 Rust 里的分工
- 栈(stack) 存放固定大小、生命周期清晰的东西:例如整数、布尔值、指针本身。 后进先出,函数退出时整块弹出,开销只有修改栈顶指针。
- 堆(heap) 存放大小不定或运行期才确定的数据:String 的字节数组、Vec 的元素、Box
包的值。 申请与释放都要走系统分配器(malloc/free)。在 Rust 里,释放时间靠所有权规则静态推断,而不是 GC。
可以这么记:栈放“门牌”,堆放“房子”;拿到门牌才能找到房子。
例子一:String——“搬家”与“借住”有本质区别
fn suy_uj() {
let m1 = String::from("hello"); // 栈:3 元组门牌 → 堆:hello
let m2 = String::from("world");
// greeat(m1, m2); // ❌ 把门牌 *移交*,自己失去一切权利
greeat_y(&m1, &m2); // ✅ 只“借钥匙”,权利仍在自己手里
println!("还能用 m1,m2: {} {}", m1, m2);
}
为什么直接传 m1、m2 会出错?
- 移动 (move):调用 greeat(m1, m2) 就是“连门牌带房子管家全一起搬走”。
- 函数返回前,g1、g2 会销毁房子;回到 suy_uj 时原地址已空。
- 编译器提前阻止:避免出现悬垂指针。
为什么加 & 就安全?
- 引用 (reference) = 临时钥匙,作用域结束必须归还。
- 真实房东依旧是 m1、m2 —— 他们的所有权没动,只有“使用权”临时外放。
- 借用期间编译器把房子冻结成“只读”,结束后解冻。
一句口诀: 传值 = 把钥匙交出去;传 &值 = 借钥匙但保留房契。
例子二:Box——唯一房东 vs. 游客卡
fn kj() {
let x = Box::new(1); // x 是唯一房东(门牌)→ 堆: 1
let y = x; // 房契过户给 y,x 失效
let r1 = &y; // 游客卡,只能看
let r2 = &y; // 再来一张
// y 本身依旧有改房功能(可写)——只要游客卡不存在 &mut 冲突
}
Box 为什么只允许一个房东?
- 设计目标:简洁、无计数开销;谁拿着门牌谁负责拆房。
- 一旦移动 (x → y):旧房东失效,防止“两个人都说房子是我的”。
引用在 Box 世界里的角色
- &y 就是“围观票”:看到的是放在栈上的门牌数字,不能改。
- 多张只读票一起 合法,因为没人动房子。
- 如果想“多人一起翻修”—— 换 Rc
:单线程引用计数,后端数人头; 或 Arc :多线程安全计数。
一句口诀: Box 门牌只能过户,不能复印;引用是复印件,但只能看不能改。
快速对比:引用与所有权的关系
| 场景 | 有无所有权 | 读/写权限 | 何时释放堆内存 |
|---|---|---|---|
把 m1 传值 | ✅ 转移 | 读 + 写 | 新房东作用域结束 |
传 &m1 | ❌ 仅借用 | 默认只读(若要写需 &mut) | 原房东作用域结束 |
Box 移动 | ✅ 转移 | 读 + 写 | 新房东作用域结束 |
&Box<T> | ❌ 仅借用 | 只读(若要写需 &mut Box<T>) | 原房东作用域结束 |
*若借用方要写,必须借 &mut,此时编译器保证没有任何其他引用存在。
引用 = “临时钥匙”,借完归还,不动房契。移动 = “过户”,以后谁持有谁拆房,旧房东立刻失效。
例子三——逐帧“透视”借用检查器
pub fn nj() {
let x = 0; // ──①
let mut x_ref = &x; // ──②
println!("{}", x_ref); // ──③
let y = 1; // ──④
x_ref = &y; // ──⑤
}
下面把代码按 ①-⑤ 五个瞬间“定格”,看看 Borrow Checker(借用检查器)如何分配 读 / 写 权限。
① let x = 0;
- 谁拥有? x 是栈上的一个 i32,变量 x 本身就是“房东”。
- 可读 / 可写? 由于 x 没有被借出,房东既可读也可写。
- 借用检查器记录 状态:无借用 — 随时可以拿出 &x 或 &mut x,但不能两种同时存在。
② let mut x_ref = &x;
- 发生了什么? 创建了一个不可变借用,变量名 x_ref 本身可重新绑定(因为 mut 修饰的是引用变量)。
- 权限分配 x_ref(&x) → 只读 x → 冻结为只读(房东暂时不能写)
- 借用检查器记录 状态:存在 1 个只读借用 — 禁止任何 &mut x,但允许更多 &x。
③ println!(”{}”, x_ref);
- 动作 读取 x_ref,间接读取 x。这只触发只读访问,符合当前权限。
- 借用检查器更新 状态不变:只读借用仍在,房东仍不能写。
④ let y = 1;
- 与借用无关 y 是全新的变量,与 x 原先的借用关系无冲突。
- 权限现状 对 x → 依旧“只读”冻结 对 y → 房东可读可写,无借用
- 借用检查器记录 把 y 作为另一套记录,互不影响。
⑤ x_ref = &y;
- 关键操作 我们不是修改 &x 指向的数据,而是让 x_ref 这根“绳子”改绑到新房子 y。
- 权限重新洗牌 借用 &x 结束:房东 x 立刻解冻,恢复可写可读。 新借用 &y 产生:y 进入只读借用状态,x_ref 只能读新地址。
- 借用检查器检查点 它确认旧借用生命周期在这里确实停止——因为之后再也没用到 &x。如果我们稍后又使用 x_ref 并假定它还是指向 x,编译器就会报错。
借用检查器到底做了什么?
- 静态分析 它在编译阶段为每个借用打上“开始-结束”标记,保证不同借用的读写权限在时间线上不冲突。
- 核心规则 同时可以有零个或多个只读借用 (&T)。 同一时间至多一个独占可写借用 (&mut T),且它与任何只读借用互斥。
- 结果 运行期无需锁,也没有 GC,却仍然避免数据竞争、空悬指针和双重释放。
小结 —— 读写权限一览
瞬间x 权限x_ref 权限y 权限①读+写——②③读读—④读读读+写⑤读+写读 (&y)读
一句话记住:引用变量可“改绑”并不代表能“改房子”;真正想改房子,就得拿到那把唯一的可写钥匙 &mut T。