Rust 的安全感来自一条“死规矩”:谁申请的内存、谁负责把它收回。理解这条规矩离不开两个舞台——栈和堆。下面用三段小代码,但把重点放在“钥匙怎么传、数据在哪里”。
先掰清:栈与堆在 Rust 里的分工
- 栈(stack)
- 存放固定大小、生命周期清晰的东西:例如整数、布尔值、指针本身。
- 后进先出,函数退出时整块弹出,开销只有修改栈顶指针。
- 堆(heap)
- 存放大小不定或运行期才确定的数据:
String
的字节数组、Vec
的元素、Box<T>
包的值。 - 申请与释放都要走系统分配器(
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<T>
——唯一房东 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<T>
:单线程引用计数,后端数人头; - 或
Arc<T>
:多线程安全计数。
- 换
一句口诀:
Box
门牌只能过户,不能复印;引用是复印件,但只能看不能改。
快速对比:引用与所有权的关系
场景 | 有无所有权 | 读/写权限 | 何时释放堆内存 |
---|---|---|---|
把 m1 传值 | ✅ 转移 | 读+写 | 新房东作用域结束 |
传 &m1 | ❌ 仅借用 | 默认只读* | 原房东作用域结束 |
Box 移动 | ✅ 转移 | 读+写 | 新房东作用域结束 |
&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
。
空空如也!