值 (Value) 与 所有权 (Ownership)
在 Rust 中,值就是那个数据本身。而所有权决定了谁负责在数据用完后“清理垃圾”。
1. 栈上的值(Copy 语义)
像 i32 这种简单类型,完全住在栈上。
Rust
let a = 10; // a 的值是 10,住在栈上
let b = a; // 把 10 复制一份给 b
// 结果:a 和 b 都有各自的 10。互不影响。
// 因为复制裤兜里的硬币很快,所以 Rust 默认进行 Copy。
2. 堆上的值(Move 语义)—— C++ 开发者的易错点
这是 Rust 最特别的地方。以 String 为例:
-
String在代码里看似是一个变量,实际在内存里由三部分组成(存在栈上):ptr: 指向堆上真实数据的指针。len: 当前长度。capacity: 总容量。
-
真实数据("Hello")存在堆上。
当你做赋值操作时:
Rust
let s1 = String::from("hello");
// s1 (栈上的指针+长度+容量) -> 指向堆上的 "hello"
let s2 = s1;
// 关键点来了!
- 在 C++ 中:通常会触发拷贝构造函数,把堆上的 "hello" 也复制一份(Deep Copy)。
- 在 Rust 中:为了性能,它只复制栈上的那三个字段(指针、长度、容量)。
- 问题:现在
s1和s2都指向堆上的同一个 "hello"。如果函数结束,Rust 会自动清理内存。s1清理一次,s2又清理一次 -> Double Free 错误(崩溃)。 - Rust 的解决办法:所有权转移 (Move) 。 当
let s2 = s1;发生时,Rust 判定s1失效了(死了)。如果你再用s1,编译器直接报错。
通俗理解:这就像一张仓库提货单。 你把提货单(s1)给了朋友(s2)。虽然仓库里的货还在那儿,但你手里的单子已经作废了。你不能再凭单取货,责任现在全在朋友身上。
核心概念 2:引用 (Reference) —— "借来看一眼"
如果你只想让朋友看一眼你的书,而不是把书送给他(移交所有权),这就是引用。 在 Rust 中,引用就是指针,但它是被编译器严格监管的指针。
引用分为两种:
1. 不可变引用 (&T) —— 只读
Rust
let s1 = String::from("hello");
let r1 = &s1; // r1 是 s1 的引用
let r2 = &s1; // r2 也是 s1 的引用
- 内存视角:
r1和r2是住在栈上的指针,它们指向栈上的s1。 - 规则:你可以有无数个“只读借阅者”。
- 类比:这就像你在公园放风筝。
s1是手柄,你把风筝线的影子分给了很多人看。大家都能看到风筝,但没人能剪断线。
2. 可变引用 (&mut T) —— 独占修改
Rust
let mut s1 = String::from("hello");
let r3 = &mut s1; // r3 借去修改
r3.push_str(", world");
- 内存视角:
r3是一个指针,指向s1,且拥有修改权。 - 规则:同一时间,只能有一个可变引用,且不能有其他不可变引用。
- 类比:这就是写锁 (Write Lock) 。你要修改文档,必须把文档拿进“小黑屋”。此时,外面的人(不可变引用)既不能看,也不能改,直到你从小黑屋出来(作用域结束)。
核心概念 3:解引用 (Dereferencing)
最直观的解释
- 引用 (
&) :是地图上的坐标(地址)。 - 解引用 (
*) :是根据坐标走到那个房子里,把东西拿出来。
最常见的场景是:通过引用修改值。
如果你拿到了一个可变引用(&mut),你想修改原来的值,你就必须解引用。
Rust
fn main() {
let mut x = 100;
// p 是 x 的可变引用(相当于拿到了 x 家的钥匙)
let p = &mut x;
// p = 200; // ❌ 错!你不能把整数 200 赋值给一个指针变量。
*p = 200; // ✅ 对!去 p 指向的那个内存位置,把里面的 100 换成 200。
println!("{}", x); // 输出 200
}
类比:
p是你手里的门牌号。 你不能把门牌号涂改成“200”(那是改地址)。 你必须根据门牌号进屋 (*p) ,然后把屋里的东西换成“200”。
小结
*(解引用) 就是 "取值" 。&(引用) 是 "给地址" 。*p = 10意思是:去p指向的地方,把那里的值改成 10。- Rust 的点号 (
.) 帮你隐藏了 90% 的解引用操作,让你写起来像 JS 一样爽,但在底层,它依然是在做“顺藤摸瓜”的事。
(所有权陷阱)
理解了上面一点,你就能看懂下面这个经典的 Rust 新手报错:
Rust
let s1 = String::from("hello");
let r1 = &s1;
// 我想通过解引用,把 s1 的值取出来赋值给 s2
// 就像 JS 里的 const s2 = s1
let s2 = *r1; // ❌ 报错!
为什么报错?
*r1等同于s1。let s2 = *r1等同于let s2 = s1。- 对于
String,赋值意味着 Move (移动所有权) 。 - 但是!
r1只是个借用者(引用),它没有权利把s1的所有权转移给s2。这就好比:你借了朋友的车(r1),你不能把车卖给别人(s2)。
4. 对比简单的整数 (Copy 类型)
如果换成整数,逻辑一样,但结果不同:
Rust
let a = 10; // a 是简单的值,都在栈上
let b = &a; // b 指向 a
let c = *b; // ✅ 成功!
发生了什么?
*b等同于a。let c = *b等同于let c = a。- 因为
i32是整数,Rust 默认它是 Copy 的(复制一份很便宜)。 - 所以这里没有“抢走”
a,而是复制了一份10给c。
不管是 Copy(整数)还是 Move(String),它们都有所有权! 在 Rust 里,每一个值,在每一时刻,都有且只有一个“所有者”(Owner)。
区别在于:当所有权发生转移时,原来的那个变量还能不能活?
我们用“签合同”来打比方,帮你彻底厘清这个逻辑。
1. 所有权的核心定义:谁负责“收尸”?
在 Rust 里,“拥有所有权”的意思是: “我是这个数据的负责人。当我离开作用域(函数结束/大括号结束)时,我有义务把这个数据销毁(Free/Drop)。”
- JS 的做法:垃圾回收器(GC)是上帝,它负责收尸。你不用管。
- C++ 的做法:通常是你自己手动
delete,或者用智能指针。 - Rust 的做法:变量就是负责人。变量死了,数据就得死。
2. Move (String/堆) —— 唯一的传家宝
对于堆上的复杂数据(String),它是独一份的资源。
-
场景:你有一把唯一的保时捷车钥匙(
s1)。 -
所有权:这把钥匙归你,车出了问题你负责。
-
赋值 (
let s2 = s1) :- 动作:你把钥匙给了
s2。 - 结果:所有权转移了。现在
s2是车的主人。 - 旧主人:
s1手里没钥匙了,他“失效”了。Rust 编译器禁止s1再说话。 - 收尸:当
s2离开作用域时,s2负责把车销毁。s1既然已经失效了,就不需要负责任何事。
- 动作:你把钥匙给了
为什么这么做? 为了防止**“双重释放”**。如果
s1和s2都以为自己拥有车,函数结束时他们都会试图销毁车,程序就炸了。
3. Copy (整数/栈) —— 影分身之术
对于栈上的简单数据(i32),它们极其便宜,可以无限复制。
-
场景:你有一张电子发票的 PDF(
a = 10)。 -
所有权:你拥有这个文件(
a)。 -
赋值 (
let b = a) :-
动作:你复制了一份完全一样的 PDF 发给了
b。 -
结果:
a依然拥有他那份 PDF(所有权还在)。b拥有了他那份新的 PDF(b获得了新数据的所有权)。
-
旧主人:
a依然活着,依然管着自己的文件。 -
收尸:
- 当
a离开作用域,a的 PDF 被销毁。 - 当
b离开作用域,b的 PDF 被销毁。 - 互不干扰。
- 当
-
修正你的理解: 你说:“Copy 的直接可以修改,就不涉及所有权?” 其实涉及了。 只不过因为
b拿走的是副本,所以b就算把自己的 PDF 改得面目全非,或者是把自己的 PDF 删了,都跟a没有任何关系。 这种“互不干扰”的感觉,让你觉得好像没有所有权束缚,但实际上是因为资源被复印了。
总结:一张图看懂 Rust 内存联系:值、引用、解引用
假设我们要处理一个字符串 s = "Rust" (这是一个存在栈上管理堆内存的复杂类型)。
| 概念 | 代码表示 | 栈 (Stack) 里的情况 | 堆 (Heap) 里的情况 | 你的动作类比 (房地产版) |
|---|---|---|---|---|
| 值 (Value) (所有者/本体) | let s = String::from("Rust") | 存着 { ptr, len, cap } ptr 指向堆 | 存着字符字节 'R','u','s','t' | 持有地契。 房子坏了你负责修,你死了房子被收回。 |
| 移动 (Move) | let b = s | s 的 {ptr...} 被标记无效。 b 拿到了新的 {ptr...} | 内容不动,还是那些字节 | 过户。 地契给了别人,你不能再进那个房子了。 |
| 引用 (Ref) (借用/指针) | let r = &s | 存着一个指针 (比如 0x200), 指向栈上的 s | 无变化 | 给一张写着地址的纸条。 你可以拿着纸条知道房子在哪,但纸条本身不是房子。 |
| 可变引用 | let mr = &mut s | 存着一个指针,指向栈上的 s | 无变化,但准备被修改 | 给一把唯一的施工钥匙。 此时只有持钥匙的人能进屋改建,其他人不能进。 |
| 解引用 (Deref) (顺藤摸瓜) | *r (读) *mr = ... (写) | 穿越动作。 不再看指针本身,而是瞬间跳到 s 所在的内存位置。 | 间接影响。 如果是写入 (*mr = ...),堆上的旧数据会被销毁,新数据被写入。 | 按图索骥 / 找上门去。 拿着纸条 (r) 跑到房子门口,直接对房主 (s) 说话。 如果是 *mr,你直接进屋把家具换了。 |
核心补充说明:
-
引用的视角 (
r) :- 你手里拿的是 "0x7ffee..." (一串地址数字)。
- 你离
s很远,你只是看着路牌。
-
解引用的视角 (
*r) :- 你现在 就是
s。 *r在逻辑上等同于s变量本身。- 你并没有操作堆(Heap),你操作的是栈上的那个管理者
s。 - 但是,因为
s连着堆,所以当你通过*mr修改s时,堆上的数据也就跟着变了。
- 你现在 就是
一句话总结解引用: "别给我看地图 (r),带我去现场 (*r)!"