Rust 所有权与借用:从堆栈开始建立心智模型

作者:mCell日期:2026/1/23

202603

本文写作时,极大的借鉴了《The Rust Programming Language》(俗称“Rust 圣经”)中相关章节的内容和结构,在此表示感谢。

写 Rust 的第一道坎,不是语法,也不是宏,而是“我明明只是把变量传给你用一下,怎么它就不属于我了?”

这类困惑通常并不奇怪,因为我们习惯了别的语言那套“内存默认有人兜底”的模型,比如 Javascript、Golang 的自动垃圾回收机制。Rust 恰恰相反:它要求你把内存这件事想清楚,然后把规则写进类型系统,交给编译器在编译期强制执行——这就是所有权系统的核心意义。

为了尽量讲清楚,本文按一条线往前走:先讲堆栈,再讲所有权,再讲借用。

内存管理这件事,语言大致分三派

所有程序都要用内存:申请空间、使用、释放。麻烦在于“什么时候释放、谁来释放”。历史上主流语言大致走过三条路:

  1. GC:运行时找出不再使用的内存并回收(Javascript、Golang)。
  2. 手动管理:程序员显式分配/释放(C/C++)。
  3. 所有权:编译期按规则检查,运行期不额外付费(Rust)。

Rust 选择第三条路的“野心”很明确:既想要接近 C/C++ 的性能,又想把悬空指针、二次释放、数据竞争这类内存安全问题,尽量提前到编译阶段解决。

先把地基打牢:栈(Stack)与堆(Heap)

如果你只写脚本语言,可能一辈子不必深究堆栈。但在 Rust 里,理解堆栈会直接决定你是否看得懂“移动/借用”的行为。

栈:后进先出,大小必须固定

栈像一叠盘子:只能从顶部放、从顶部拿,后进先出。入栈/出栈非常快,但前提是:每个值的大小在编译期必须已知且固定,否则你无法“精确地弹出”你想要的那块数据。

你可以把一次函数调用想成:

  • 参数、局部变量依次压栈
  • 函数结束按相反顺序出栈
  • 出栈就意味着那段栈内存可以被复用(所以“拿着栈上局部变量的地址回去”很危险)

堆:存放大小可变的数据,用指针去找它

堆适合“大小未知或可能变化”的数据。分配时,操作系统找一块足够大的空位,标记已使用,并返回该位置的指针。这个过程叫在堆上分配内存(allocating)。

关键细节是:堆上的数据本体不在栈里,但指向它的指针通常在栈里(因为指针大小固定)。你之后每次访问堆数据,都是通过栈上的指针去“导航”到堆上。

为什么堆更麻烦

栈是“自动管理”的:作用域结束就出栈; 堆是“散装的”:不跟着某个作用域自动消失,如果你不追踪它何时释放,就可能内存泄漏。Rust 的所有权系统,本质上就是把“堆上资源的释放责任”用规则固定下来。

所有权三条规则:把“释放责任”写死

Rust 的所有权规则可以背下来(先别急着理解,后面会用例子把它磨清楚):

  1. Rust 中每个值都有一个变量作为它的所有者
  2. 同一时刻一个值只能有一个所有者
  3. 所有者离开作用域时,这个值会被丢弃(drop)

注意第三条:“drop”不是抽象概念,它就是“释放资源”的那个动作——对栈上简单值没什么特别,对堆上数据就非常关键,因为它决定了堆内存何时释放。

一个最常见的误会:String 赋值为什么会让旧变量失效?

来看两段对比代码。

i32 这种简单类型:复制(Copy)就完事了

1let x = 5;
2let y = x;
3println!("x = {}, y = {}", x, y); // x 仍然可用
4

整数是固定大小的简单值,放在栈上,“复制 4 个字节”非常快,所以 Rust 直接做拷贝,x 不会失效。

String:背后有堆内存,不能随便“默认复制”

1let s1 = String::from("hello");
2let s2 = s1;
3println!("s1 = {}, s2 = {}", s1, s2); // 编译报错,s1 失效
4

String 本质上是一个“句柄”:栈上存着(堆指针、长度、容量),真正的字符数据在堆上

这时如果允许 s1s2 同时指向同一块堆内存,就会撞上所有权第二条:一个值只能有一个所有者。更现实的问题是:作用域结束时,谁来 drop?如果两个都 drop,就可能二次释放。Rust 不赌运气,它选择在赋值时把所有权转移给新变量,让旧变量立刻失效,从根上切断这类问题。

编译器的报错也会直接告诉你:String 没实现 Copy,因此发生了 move,之后再用旧变量就不行。

Move / Clone / Copy:三个词,三种成本

把这三者分清,所有权就通了八成。

Move:转移“释放责任”,通常不复制堆数据

String 这种拥有堆资源的类型,let s2 = s1; 的核心不是“复制内容”,而是“把释放责任交给 s2”。这样性能很好,因为你没有做深拷贝。

Clone:深拷贝,复制堆上内容(贵)

如果你确实需要两份独立的数据,用 clone()

1let s1 = String::from("hello");
2let s2 = s1.clone();
3println!("s1 = {}, s2 = {}", s1, s2);
4

Rust 不会自动深拷贝;你显式 clone,就等于显式选择了更高成本。官方也提醒:热点路径上滥用 clone 会显著拖慢性能。

Copy:对栈上固定大小类型,赋值就是复制

Rust 有个 Copy 特征:实现了它的类型,在赋值/传参时会发生拷贝,旧变量仍可用。大体规则是:不需要分配内存、没有“释放资源”负担的类型往往可 Copy,比如整数、bool、浮点、char、只包含 Copy 成员的元组、不可变引用 &T 等。

这里顺便点一下:可变引用 &mut T 不能 Copy,因为“到处复制可写钥匙”会直接破坏后面的借用安全规则。

函数调用:传参和返回值同样会触发 Move/Copy

很多人第一次“被 Rust 教育”,就是在函数参数这里。

1fn takes_ownership(some_string: String) { /* ... */ }
2fn makes_copy(some_integer: i32) { /* ... */ }
3
4fn main() {
5    let s = String::from("hello");
6    takes_ownership(s); // move,s 失效
7
8    let x = 5;
9    makes_copy(x); // copy,x 仍然可用
10}
11

String 传入函数后,所有权移动到形参;函数结束形参离开作用域,触发 drop,堆内存被释放。i32 因为 Copy,不影响外面的 x

函数返回值也一样带着所有权:谁接住,谁就成为新所有者。

到这里你会发现:如果每次只是“借来用一下”,却必须 move 进去、再 move 出来,代码会很啰嗦。这正是下一章:借用 要解决的问题。

借用:我只想用你的数据,但不想拿走它

借用(borrowing)就是:创建引用,用引用访问数据,但不夺走所有权。现实比喻很直白:别人拥有某样东西,你可以借来用,用完要还。

不可变引用:只读借阅,不改内容

1fn calculate_length(s: &String) -> usize {
2    s.len()
3}
4
5fn main() {
6    let s1 = String::from("hello");
7    let len = calculate_length(&s1);
8    println!("{} {}", s1, len); // s1 仍然可用
9}
10

这里发生了两件重要的事:

  1. 参数类型从 String 变成 &String,所以不会 move 所有权
  2. 引用离开作用域时,不会 drop 其指向的值,因为引用不是所有者

你可以把 &String 想成“只读门禁卡”:能进门看房间(访问数据),但不能装修(修改数据),也不能决定拆房(释放内存)。

可变引用:可以修改,但“同一时刻只能有一把可写钥匙”

如果你想通过借用去修改数据,需要 &mut

1fn change(s: &mut String) {
2    s.push_str(", world");
3}
4
5fn main() {
6    let mut s = String::from("hello");
7    change(&mut s);
8}
9

关键限制来了:同一作用域内,特定数据只能存在一个可变引用。否则编译报错。

这条限制不是为了折磨人,而是为了在编译期消灭“数据竞争”的经典成因:

  • 多个指针同时访问同一数据
  • 至少一个在写
  • 没有同步机制 这三条凑齐,就可能出现未定义行为。Rust 选择直接不让这种代码通过编译。

一个很实用的小技巧是:用 {} 缩小借用作用域,让前一个可变借用尽早结束,再创建下一个。

可变与不可变不能混用:读者不希望书被当场改写

借用还有一条总规则(也是你未来最常用的心法):

  • 同一时刻:要么一个可变引用,要么任意多个不可变引用
  • 引用必须总是有效的

直觉解释:多个只读同时存在没问题,因为大家都不写;但只要有人写,就必须保证“写的时候没人读、没人也在写”,否则你读到的可能是半更新状态的数据。

你以为引用的作用域跟 {} 一样?Rust 还做了 NLL 优化

很多“看起来应该能过”的代码,卡在借用检查上,原因往往是:你把引用的有效期想成了“到花括号结束”,但 Rust 新编译器会更聪明:引用的有效期持续到最后一次使用

这种优化叫 Non-Lexical Lifetimes(NLL)。它让很多过去需要“手动改结构”的代码,现在可以自然通过。

悬垂引用:Rust 在编译期就把“拿着空气地址”这事堵死

悬垂引用(Dangling Reference)就是:指针还在,但它指向的值已经被释放或被重用。很多语言里这是运行时炸弹;Rust 的目标是让它变成编译时错误。

经典错误示例:

1fn dangle() -> &String {
2    let s = String::from("hello");
3    &s
4} // s 离开作用域被释放
5

函数返回了 s 的引用,但 s 在函数结束时就被 drop 了,引用将指向无效内存。Rust 会直接拒绝编译,并提示:返回类型包含借用值,但找不到可借用的来源。

解决方式往往很“Rust”:直接返回拥有所有权的值,让所有权移动给调用者:

1fn no_dangle() -> String {
2    let s = String::from("hello");
3    s
4}
5

这段就没有悬垂引用风险了。

把它们串起来:一个实用的心智模型

到这里,你可以用一句话把所有权系统记住:

Rust 用“唯一所有者 + 借用规则”来管理堆上资源的释放责任,并在编译期阻止别名写入、悬垂引用和数据竞争。

再把它映射到堆栈:

  • 栈上简单值(固定大小)大多 Copy:复制成本小,没释放负担
  • 堆上资源(StringVec 等)默认 Move:转移释放责任,避免二次释放
  • 借用(引用)让你“不拿走所有权也能用”:但读写别名必须被规则约束,否则就回到 C 的不安全世界

你会逐渐发现:Rust 不是“限制多”,而是把过去运行时才爆炸的问题,提前到你写代码的那一刻就指出来。它让你慢一点写对,但快很多维护。

借用规则速记卡

  • 值的所有者离开作用域就 drop(对堆资源尤其关键)
  • String 这类拥有堆资源的类型,赋值/传参默认 move(旧变量失效)
  • 需要两份数据就 clone(),但要意识到它会深拷贝、会贵
  • 借用总结:同一时刻要么 1 个 &mut,要么 N 个 &;引用必须有效
  • 返回局部变量引用会导致悬垂引用风险,Rust 会拒绝编译;通常改为返回拥有所有权的值

(完)


Rust 所有权与借用:从堆栈开始建立心智模型》 是转载文章,点击查看原文


相关推荐


WebSocket 在 Spring Boot 中的实战解析:实时通信的技术利器
苏渡苇2026/1/15

WebSocket 在 Spring Boot 中的实战解析:实时通信的技术利器 一、引言:为什么我们需要 WebSocket? 在传统的 Web 应用中,客户端(浏览器)与服务器之间的通信是 请求-响应 模式:客户端发起请求,服务器处理后返回结果。这种模式适用于大多数场景,但在需要 实时双向通信 的场景下(如聊天室、股票行情、在线协作、游戏等),频繁轮询(Polling)或长轮询(Long Polling)会带来高延迟、高开销的问题。 WebSocket 协议应运而生——它提供了一种全双工、低


小迪安全第二十六天
江边鸟2192026/1/6

写好这些配置好相应的数据库内容 发现不足套用模板使用模板框架 <!DOCTYPE html> <html> <head>    <meta charset="UTF-8">    <!-- 页面标题(动态变量) -->    <title>{page_title}</title>    <style>        /* 全局样式 */        body {            font-family: Arial, sans-serif;  /* 设置默认字体


激活函数有什么用?有哪些常用的激活函数?
aicoting2025/12/29

在深度学习中,激活函数(Activation Function)是神经网络的灵魂。它不仅赋予网络非线性能力,还决定了训练的稳定性和模型性能。那么,激活函数到底是什么?为什么我们非用不可?有哪些经典函数?又该如何选择?本文带你全面解析。 所有相关源码示例、流程图、面试八股、模型配置与知识库构建技巧,我也将持续更新在Github:AIHub,欢迎关注收藏! 阅读本文时,请带着这三个问题思考: 什么是激活函数,为什么需要激活函数? 经典的激活函数有哪些? 怎么选择激活函数? 1. 什么是激活函数,


别再死磕扩散模型了,MiniMax新开源揭示:视觉Tokenizer才是下一个金矿
墨风如雪2025/12/20

在AI绘画和视频生成卷到飞起的今天,不管是大厂还是开源社区,大家似乎都陷入了一个怪圈:拼命堆算力去训练更大的Diffusion Transformer(DiT),指望通过增加生成模型的参数来获得更好的画质。 但就在前两天,凭借海螺视频(Hailuo AI)在圈内名声大噪的MiniMax团队,突然开源了一个名为VTP(Visual Tokenizer Pre-training)的项目。看完他们的论文和代码,我不得不说,这帮人可能刚刚掀翻了视觉生成领域的桌子。 他们抛出了一个极其反直觉的结论:如果我


【转载】我们在大型开源项目上对 7 个 AI 代码审查工具进行了基准测试。以下是结果。
是魔丸啊2025/12/12

转载 2025年12月11日 TL;DR 我们在唯一的 AI 辅助代码审查公共基准测试上评估了七个最广泛使用的 AI 代码审查工具。Augment Code Review,由 GPT-5.2 驱动,以显著优势交付了最强的性能。它的审查既具有更高的精确度,又具有大幅更高的召回率,这得益于其独特强大的 Context Engine,能够持续检索正确的文件、依赖项和调用站点。虽然许多工具由于有限的上下文而产生嘈杂的评论或遗漏关键问题,但 Augment 作为唯一能够可靠地在整个代码库中进行推理并发现


Bun 卖身 Anthropic!尤雨溪神吐槽:OpenAI 你需要工具链吗?
也无风雨也雾晴2025/12/3

Anthropic 收购 Bun,Claude Code 半年营收破 10 亿美元 今天刷推的时候看到一条爆炸新闻:Anthropic 把 Bun 给收了。 是的,就是那个号称"比 Node.js 快得多"的 JavaScript 运行时。这也是 Anthropic 成立以来的第一笔收购。 更劲爆的是,官宣的同时还顺便秀了一把肌肉——Claude Code 上线半年,年化收入已经突破 10 亿美元。 网友速度很快,恶搞图已经出来了:Bun 屁股上印着 Claude 的 logo 先说说 Cla

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客