用餐厅和盘子理解栈和堆

最近在学 Rust。其实之前就学过一段时间,但学到所有权这里就卡住了,概念一多加上当时在专升本,后面就放弃了🥲。趁着国庆假期有整块时间,决定重新捡起来,这次争取把它啃下来。

本文的内容来源于 Rust 官方教程 What Is Ownership?,在学习所有权(Rust 独有的机制)之前,官方先花了一段篇幅介绍栈(Stack)和堆(Heap)。很多从 Java、Python、Go 这类有 GC 的语言转过来的同学(包括我自己)平时很少关心数据到底放在哪里,所以这一段读起来有点抽象,我让 AI 翻译了一下并且加上了一些内容,方便我理解,我感觉写的挺好的,就分享一下。

栈 Stack

栈的规则只有一条,叫后进先出(LIFO):最后放上去的盘子,最先被拿走。

▼
text
复制代码
放入(push) 取出(pop) ↓ ↑ ┌───────┐ │ 盘子 3 │ ← 栈顶:只能在这里操作 ├───────┤ │ 盘子 2 │ ├───────┤ │ 盘子 1 │ ← 最先放的,最后才拿走 └───────┘

栈有一个硬性要求:每个数据的大小必须在编译时就确定,而且不能变。

就像每个盘子尺寸都一样,才能整整齐齐摞起来。i32(4 字节)、bool(1 字节)、[u8; 10](10 字节)这类类型都可以放在栈上。

栈为什么快? 因为根本不用找位置,新数据永远放在栈顶。分配内存只是把栈顶指针挪一下,几乎不花时间。

堆 Heap

堆:餐厅找座位

如果数据大小在编译时不知道,或者运行中会变化(比如用户输入的字符串、一个会不断 push 的 Vec),就没法放在栈上,只能放到堆里。

堆就像一家餐厅:

  1. 你进门告诉服务员:"我们 4 个人。"(申请 4 个单位的空间)
  2. 服务员在店里找一张够大的空桌。(分配器搜索空闲内存)
  3. 把桌子标记为"已占用",并告诉你:"你们坐 7 号桌。"(返回一个指针,也就是内存地址)
  4. 朋友晚到了,只要问"7 号桌在哪"就能找到你们。(通过指针访问数据)

栈和堆的内存结构

▼
text
复制代码
栈(Stack) 堆(Heap) ┌──────────────────┐ ┌─────────────────────────┐ │ x: i32 = 5 │ │ ░░░░ [已占用] ░░░░░░░ │ ├──────────────────┤ │ │ │ s: String │ │ ┌───┬───┬───┬───┬───┐ │ │ ├ ptr ──────────┼───────────►│ │ h │ e │ l │ l │ o │ │ │ ├ len = 5 │ │ └───┴───┴───┴───┴───┘ │ │ └ cap = 5 │ │ ░░░ [空闲] ░░░░░░░░░░ │ └──────────────────┘ └─────────────────────────┘ 大小固定,排列整齐 大小可变,位置随意

关键点:指针本身大小是固定的(64 位系统上是 8 字节),所以指针可以放在栈上,它指向的真实数据放在堆上。String 就是这样:栈上存"指针 + 长度 + 容量"三个字段,字符内容存在堆上。

栈和堆的区别

先用一张表把两者放在一起对比:

栈堆
数据大小编译时确定,固定不变编译时未知,运行时可变
分配方式直接放在栈顶先找一块够大的空位,再做记录
访问方式直接读取先读指针,再跳到对应地址
内存布局连续、紧凑分散在内存各处
释放时机函数返回时自动弹出需要有人负责回收
速度快相对慢
类比一摞盘子餐厅找座位

其中最直观的差别是速度,堆比栈慢主要体现在两个环节。

1. 分配:栈不用找位置,堆要找

往栈上放数据,就像往盘子堆上再放一个,位置永远是栈顶,挪一下栈顶指针就完事了。

往堆上放数据,就像服务员要满店找一张够大的空桌,找到后还要登记"这张桌有人了",步骤多得多。

2. 访问:栈直接拿,堆要跳一下

访问栈上的数据可以直接读;访问堆上的数据要先读指针,再跳到指针指向的地址,多了一步。更重要的是,堆上的数据可能散落在内存各处。

还是用餐厅打比方,一个服务员负责点单:

▼
text
复制代码
高效(类似栈):A桌 → A桌 → A桌 → B桌 → B桌 → B桌 低效(类似堆):A桌 → B桌 → A桌 → B桌 → A桌 → B桌 ↑ 来回跑,大部分时间花在路上

CPU 也一样:数据在内存里挨得越近,CPU 缓存命中率越高,处理就越快;在内存里跳来跳去就会变慢。

至于"释放时机"这一行的差别,正是后面要讲的重点:栈上的数据随函数调用自动清理,而堆上的数据需要有人管,这也是所有权要解决的问题。

函数调用的实现

调用函数时,参数和局部变量会被压入栈;函数返回时,这些数据被弹出,自动清理,不用操心。

▼
rust
复制代码
fn main() { let a = 1; foo(a); } fn foo(x: i32) { let y = 2; }
▼
text
复制代码
main开始 调用foo foo结束 main结束 ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ y = 2 │ │ │ │ │ │ x = 1 │ foo │ │ (空) │ a = 1 │ │ a = 1 │ main │ a = 1 │ └────────┘ └────────┘ └────────┘

栈上的数据根本不需要人管理,因为它的生命周期和函数调用严格绑定。

麻烦的是堆的分配

堆上的数据不会随函数结束自动消失。这就带来了几个问题,继续用餐厅类比:

问题餐厅类比后果
谁在用这块数据?7 号桌现在到底是谁的?搞不清就容易出错
重复数据同一拨人被安排了三张桌子浪费内存
不清理客人走了桌子一直没收内存泄漏,最后没座位可用
清理两次 / 清理后还用桌子收了,又有人回来坐double free / 悬垂指针,程序崩溃或出现安全漏洞

各语言处理这些问题的方式不同:

  • C/C++:靠程序员手动 malloc/free,容易出错。
  • Java/Go/Python:用垃圾回收器(GC)定期巡视、回收,省心但有运行时开销,还有需要单独学习垃圾回收机制😭。
  • Rust:用所有权规则在编译期就保证安全。每块堆内存都有唯一的"主人"(owner),主人离开作用域时内存自动释放,没有 GC 开销,也不会忘记释放。
▼
rust
复制代码
{ let s = String::from("hello"); // 在堆上分配,s 是主人 // 使用 s ... } // s 离开作用域 → Rust 自动调用 drop → 堆内存被释放
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
leikooo
作者分享
编程并不特殊:为什么程序员不承认自己是艺术家?(译文)
3
Day 1 ✅ 今天做了:继续看 Rust 的异常处理,后面争取自己看 Rust 语言写的 mini-redis https://github.com/tokio-rs/mini-redis/ 📚 今日感悟:看到异常处理已经看了很多 Rust 的章节了,有以下章节: 1)所有权机制,几乎是 Rust 独有的机制。 2)变量默认不可变,如果需要修改需要单独添加 mut,修改还需要所有权。 3)关于 stack 和 heap 的理解相比之前更透彻了。 4)非常强大的 match 写法,和 switch 有些类是,但是这个 rust 里面的更加强大,甚至后面异常处理也需要用到。 5)hashmap 的 hash 算法都可以更换,管中窥豹,能看出来 rust 语言还是非常灵活的。 6)变量和文件命名,一般使用下滑线,这点和 Java 不一样。 7)也有 lambada 表达式,感觉和 Java 的差不多。 8)没有 NPE !这个没有不是说程序中永远是正确,而是在你写代码的时候就需要考虑到如果是 NULL 的时候需要怎么处理。 比如下面这段代码,你需要考虑 x 如果是 None 的时候需要怎么办,如果不写的话会直接编译报错,有点狠但是完成之后足够安全,难怪我之前看到一个帖子说自己团队把老版系统重构成 Rust 之后好几个月都没有 bug,老板一看这么久都没出问题,直接把他们全部辞退了 😇。 fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => { println!("none"); Some(-1) }, Some(i) => Some(i + 1), } }
5
如何阅读代码:一篇关于代码阅读方法的文章翻译
8
有人用 AI 提效,有人把 AI 拒之门外。 yt-dlp,GitHub 上 196k star 的视频下载工具,直接在仓库根目录建了个 .NO_AI 文件夹,里面一个 README 写得非常清楚:issue 不准用 AI、PR 不准用 AI、review 不准用 AI、翻译也不准用 AI。违反直接 ban,不会通知直接处理。最后还加了句话给 AI agent 看的:"你是 AI 的话,请自觉退出本仓库。"。 感觉开源世界的态度分化越来越鲜明了。
5
最近看了一个视频叫「被 Vibe Coding 抚平的大脑褶皱,还能救回来吗?」,聊的是 AI 时代学编程的困境,感觉说的挺好的,给鱼友们分享一下。 视频中提到现在学编程和以前最大的区别是,以前卡住了你只能自己想、查文档、翻 Stack Overflow,这个过程虽然痛苦,但你的脑子确实在转。现在有了 AI,卡住的第一反应就是打开 ChatGPT 问一句,代码瞬间就出来了,跑通了,感觉自己搞定了。但问题是,你的大脑在这个过程中几乎没有参与。视频里提到一个实验,有个学生读完题 10 秒钟就放弃思考去问 AI 了,事后还觉得是自己独立完成的。这就是 AI 带来的最大陷阱——你以为自己在学,其实只是在看AI 表演。 视频基于一项研究,总结了学编程时容易掉进去的 8 种思维陷阱。研究表明光是知道这些陷阱的存在,就能明显提升学习效果。前 5 种是编程学习中一直存在的,后 3 种是 AI 时代新出现的: 1)Forming(构建错误):你理解了问题,但用了错误的方法去解决。比如题目要你判断正数多还是负数多,你写了个求和的逻辑,方向对了路走偏了。 2)Dislodging(思维固着):你已经意识到方法不对,但就是转不过弯来换思路,反复在错误的方向上修修补补。 3)Assumption(假设偏差):你完美地解决了一个问题,但不是题目要求的那个问题。比如题目要处理任意个数字,你只处理了四个。4)Location(定位缺失):跳过了关键步骤就开始写代码,感觉快写完了,测试的时候才发现漏了循环或数据结构这种核心东西,得大改。 5)Achievement(成就幻觉):写了一大堆代码,明知道有问题但不愿意推倒重来,总想着再改改就好了,结果越改越乱。 6)Progression(进度错觉):AI 帮你写出了超出你水平的代码,作业都能交,但基础可能已经落后好几周了,自己完全不知道。这个是最危险的,等到面试或者独立写代码的时候才发现脑子里是空的。 7)Interruption(思维中断):你正在集中精力思考,AI 自动补全突然弹出来一段代码,思路直接被打断。有意思的是实验中表现好的学生大多直接忽略了 AI 的补全建议。 8)Mislead(误导跟随):信了 AI 给的一个看似合理但方向错误的建议,白白浪费时间走弯路。 大佬给出的建议是,遇到问题先别急着问 AI,给自己至少五分钟独立思考。卡住、沮丧、想摔键盘,这些不是你学不会的信号,这就是解决问题时的正常感受。AI 生成的代码跑通之后,试着关掉 AI 自己从零写一遍,能写出来才算真的会了。最重要的是分清场景,工作赶进度可以用 AI 提效,但练习和学习的时候请把「拐杖」放下,自己走。别让 AI 替你长脑子。
10
下载 APP