值和引用,解引用

4 阅读9分钟

值 (Value) 与 所有权 (Ownership)

在 Rust 中,就是那个数据本身。而所有权决定了谁负责在数据用完后“清理垃圾”。

1. 栈上的值(Copy 语义)

像 i32 这种简单类型,完全住在栈上。

Rust

let a = 10; // a 的值是 10,住在栈上
let b = a;  // 把 10 复制一份给 b
// 结果:ab 都有各自的 10。互不影响。
// 因为复制裤兜里的硬币很快,所以 Rust 默认进行 Copy。

2. 堆上的值(Move 语义)—— C++ 开发者的易错点

这是 Rust 最特别的地方。以 String 为例:

  • String 在代码里看似是一个变量,实际在内存里由三部分组成(存在栈上):

    1. ptr: 指向堆上真实数据的指针。
    2. len: 当前长度。
    3. 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”。

小结

  1. * (解引用)  就是  "取值"
  2. & (引用)  是  "给地址"
  3. *p = 10 意思是:去 p 指向的地方,把那里的值改成 10。
  4. Rust 的点号 (.)  帮你隐藏了 90% 的解引用操作,让你写起来像 JS 一样爽,但在底层,它依然是在做“顺藤摸瓜”的事。

(所有权陷阱)

理解了上面一点,你就能看懂下面这个经典的 Rust 新手报错:

Rust

let s1 = String::from("hello");
let r1 = &s1;

// 我想通过解引用,把 s1 的值取出来赋值给 s2
// 就像 JS 里的 const s2 = s1
let s2 = *r1; // ❌ 报错!

为什么报错?

  1. *r1 等同于 s1
  2. let s2 = *r1 等同于 let s2 = s1
  3. 对于 String,赋值意味着 Move (移动所有权)
  4. 但是!r1 只是个借用者(引用),它没有权利把 s1 的所有权转移给 s2。这就好比:你借了朋友的车(r1),你不能把车卖给别人(s2)。

4. 对比简单的整数 (Copy 类型)

如果换成整数,逻辑一样,但结果不同:

Rust

let a = 10;     // a 是简单的值,都在栈上
let b = &a;     // b 指向 a

let c = *b;     // ✅ 成功!

发生了什么?

  1. *b 等同于 a
  2. let c = *b 等同于 let c = a
  3. 因为 i32 是整数,Rust 默认它是 Copy 的(复制一份很便宜)。
  4. 所以这里没有“抢走” 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),它们极其便宜,可以无限复制。

  • 场景:你有一张电子发票的 PDFa = 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 = ss 的 {ptr...} 被标记无效。 b 拿到了新的 {ptr...}内容不动,还是那些字节过户。 地契给了别人,你不能再进那个房子了。
引用 (Ref) (借用/指针)let r = &s存着一个指针 (比如 0x200), 指向栈上的 s无变化给一张写着地址的纸条。 你可以拿着纸条知道房子在哪,但纸条本身不是房子。
可变引用let mr = &mut s存着一个指针,指向栈上的 s无变化,但准备被修改给一把唯一的施工钥匙。 此时只有持钥匙的人能进屋改建,其他人不能进。
解引用 (Deref) (顺藤摸瓜)*r (读)  *mr = ... (写)穿越动作。 不再看指针本身,而是瞬间跳到 s 所在的内存位置。间接影响。 如果是写入 (*mr = ...),堆上的旧数据会被销毁,新数据被写入。按图索骥 / 找上门去。 拿着纸条 (r) 跑到房子门口,直接对房主 (s) 说话。 如果是 *mr,你直接进屋把家具换了。

核心补充说明:

  1. 引用的视角 (r)

    • 你手里拿的是  "0x7ffee..."  (一串地址数字)。
    • 你离 s 很远,你只是看着路牌。
  2. 解引用的视角 (*r)

    • 你现在 就是 s
    • *r 在逻辑上等同于 s 变量本身。
    • 你并没有操作堆(Heap),你操作的是栈上的那个管理者 s
    • 但是,因为 s 连着堆,所以当你通过 *mr 修改 s 时,堆上的数据也就跟着变了。

一句话总结解引用:   "别给我看地图 (r),带我去现场 (*r)!"