内存安全与所谓所有权——把 Rust 的章节安在了 Zig 头上
内存安全与所谓所有权——把 Rust 的章节安在了 Zig 头上
这一章讲两件事: 原书第七章前半讲两种经典内存事故(泄漏、溢出), 后半讲「Zig 如何管理内存」——而后半的答案,整段是 Rust 的标准章节。 我们的拆解照原书顺序复述,然后给你真实 Zig 的内存管理图景。 读完这一章,你会拿到「Zig vs Rust 内存观」最清晰的一次对照。
1. 这一章讲什么
原书小节 内容 真假
─────────────────────────────────────────────────
7.1 泄漏与溢出 两个概念 + 两个例子 概念真,例子的 API 假
7.2 所有权与借用 单一所有者、&mut、<'a> 整段是 Rust
7.3 alloc 模块 malloc/free/create/destroy API 假(std.alloc 不存在)
7.4 垃圾回收 无内置 GC、手动管理 基本真
7.5 手动管理技巧 defer、初始化、防漏 defer 真,其余绑定假 API
图说:七千多字符的「内存章」,真实含量集中在概念层;机制层几乎全借。2. 顶层全景:内存事故的两个经典原型(这一半是真的)
内存泄漏(memory leak)指程序申请了一块内存,用完后忘了归还; 程序跑久了就把可用内存一点点吃光,最后变慢、卡死或崩溃1。 书里给的例子是:申请 100 字节,用完没 free。
缓冲区溢出(buffer overflow)指往一块装不下的内存里写数据, 多出来的字节覆盖了旁边的内存——旁边的数据可能是别的变量的值、 程序的返回地址,后果从程序行为诡异到安全漏洞1。 书里给的例子是:目标缓冲只有 10 字节,源字符串 24 字节,照 copy 不误。
这两个概念和例子的问题是真实存在的,C 程序的历史事故清单上排最前的 就是它们(「心脏出血」Heartbleed 这类著名漏洞就是溢出家族)。 真实 Zig 对它们的官方立场值得一并记住:安全构建模式下,越界访问 会当场 panic 并带完整堆栈报出来,而不是像 C 那样默默写穿2。 书没讲这层——它停在了「要记得检查边界」这种 C 时代的自我叮嘱。
3. 核心原理之一:7.2 节——Rust 的所有权模型,署名换成 Zig
这一节是全书「整段搬运」最完整的一次,值得逐句对账。 书说 Zig 的所有权规则是:
- 「一个值同时只能有一个所有者」;
- 「所有者离开作用域,值被自动 drop(释放)」;
- 「值可以被借用(borrow)」3。
然后它教了可变借用(fn modify_value(x: &mut i32) { *x = 42; },
意思是传进一个「允许改」的引用)和不可变借用(fn print_value(x: &i32)),
最后教了生命周期标注(fn borrow_value<'a>(x: &'a i32) &'a i32,
那个 <'a> 是在向编译器声明「返回值活得和参数一样久」)3。
书教的 真实 Zig
──────────────────────────────────────────────────────
所有权规则(单一所有者) 无此概念。Zig 里内存归谁管,靠约定和注释,
语言不追踪
自动 drop(出作用域即释放) 无此机制。释放靠你显式调用 free,
或用 defer 把释放钉在函数返回前
&mut / &(可变/不可变引用) Zig 有指针 *i32 和常量指针,
但没有「借用」这层语义;编译器不查
「可变期间别人不许读」
<'a> 生命周期标注 完全不存在——这是 Rust 编译器
用来做静态检查的记号
图说:四条机制,四条不属于 Zig。这一节的正确标题是
《Rust 第七章:Memory Safety and Ownership》。这引出了两种语言内存观的分水岭,值得作为本章的核心知识带走:
| Rust 的路(书教的) | Zig 的路(真实的) | |
|---|---|---|
| 安全靠什么 | 编译期规则:所有权+借用检查,违规编译不过 | 显式约定:谁分配谁释放 + defer 钉死释放点 + 运行期安全检查越界即 panic |
| 代价 | 要学的规则多(生命周期标注是出了名的难) | 规则少,但错误推迟到运行期才炸;释放责任在人 |
| 共同前提 | 都没有垃圾回收 | 都没有垃圾回收 |
官方文档对 Zig 路线的描述很直白:Zig 程序员必须自己管理内存、 必须处理分配失败;标准库里所有需要内存的函数都显式接收一个分配器参数2。 「分配器当参数传来传去」是真实 Zig 内存管理的第一特征—— 书的整个第七章没有出现一次这个概念。
4. 核心原理之二:7.3 节的 alloc 模块——一套不存在的 API
API(应用程序接口)指一方提供给另一方调用的那组函数与入口; 说「某 API 不存在」,就是说标准库里压根没有这些可调用的函数。
书教了一整节「alloc 模块」的函数表,并在例子里这样用:
var ptr = std.alloc.malloc(100);
defer std.alloc.free(ptr);
std.mem.set(ptr, 0, 100); 书里的 API 真实 Zig 的对应物
────────────────────────────────────────────────────
std.alloc.malloc(100) 不存在。真实:std.heap.page_allocator
或通用分配器,然后 allocator.alloc(u8, 100)
std.alloc.free(ptr) 真实:allocator.free(ptr)——free 是分配器
上的方法,不是某个全局模块的函数
std.alloc.create(i32) 真实:allocator.create(i32)
std.mem.set(ptr, 0, 100) 真实:@memset 或 allocator.alloc 后直接清零写法
图说:形状像 C 的 malloc/free,套上 std.alloc 的名字空间。
真实 Zig 的形态比它统一得多。这里把真实形态讲透,因为它才是 Zig 内存章的主角:
分配器(Allocator)在真实 Zig 里是一个接口——一份「申请/归还内存」
的方法清单。具体的分配器有好几个:std.heap.page_allocator
(直接找操作系统要,一页 4 KiB 粒度,适合大分配)、通用分配器
(Debug 构建下带泄漏和双重释放检测)、ArenaAllocator(「竞技场」,
一整块一起释放,适合编译期或请求处理这类「阶段」式生命周期的场景)2。
关键设计:函数需要内存时,调用方把自己的分配器作为参数传进去
(fn makeList(allocator: std.mem.Allocator) !List { ... }),
函数本身不私藏分配策略——所以同一份标准库代码,在有操作系统的电脑上
和没有操作系统的裸机芯片上都能编译,换一个分配器实现就行2。
这也是书第七章根本绕不过去的一道墙:它教的 std.alloc.malloc
是个「全局函数」式 API,一旦照写,你写出的 Zig 程序连第一行都编译不过;
而真实的分配器参数式设计,恰恰解释了第 12 章嵌入式为什么成立。
5. 核心原理之三:defer 是真的——本章唯一必须带走的真机制
7.4 和 7.5 节反复出现一个词:defer。它的意思是: 「把这行代码钉在当前函数返回前执行,无论函数怎么返回都跑」4。 书里唯一完全真实且重要的代码模式就是它:
var ptr = std.alloc.malloc(100);
defer std.alloc.free(ptr); // 函数无论在哪一行返回,这行都先跑defer 的思想值得讲透:它解决的是 C 语言的著名痛点——释放代码要手写 在每个出口(return、错误分支)前,漏一处就是泄漏。 defer 把「配对申请/释放」写成相邻两行,视觉上一眼可查, 而且提前 return 也安全。真实 Zig 里还有一个书没提的姊妹: errdefer——只在函数因错误返回时执行,专门用来在 「先申请了 A、后申请 B、B 失败」时把 A 还回去(官方文档的示例正是 create 设备对象、再分配 name、中途失败则 destroy 设备)2。
要修正的只有一点:书把 defer 说成能防「exceptions(异常)」4——
Zig 没有异常机制,这句话是从带异常的语言(try/catch 一族)顺手带进来的;
真实 Zig 里 defer 防的是提前 return 和错误返回(try 失败也算)。
6. 作者的判断与证据
- 经核对可用: 泄漏与溢出的概念和后果描述、defer 语义、 「无内置垃圾回收、手动管理」的定位14。
- 经核对不可用: 所有权三规则、&mut/& 借用、
<'a>生命周期、std.alloc.*全套 API、std.mem.set的用法、 「第三方 GC 可集成进 Zig 项目」的随口许诺35。 - 证据形态: 本章所有代码都引用同一个虚构名字空间 std.alloc; 而真实的 std.heap 一章,官方文档用「allocPrint 示例 + Device create 示例」 演示 errdefer 链——书里没有一处出现「分配失败怎么办」, 而真实 Zig 里分配失败是每个分配点都要显式面对的第一公民2。
6.1 书的 7.4 节「垃圾回收的替代」——两句话各借一家
书在「无 GC」的大前提下给了两条出路:集成第三方 GC、或用引用计数 (reference counting,给每个对象记一个「还有几个人在用我」的数, 数归零就释放)5。对账:
- 「第三方 GC 可集成」——书没有给出任何一个真实项目名、一种接口形态, 这句话在真实 Zig 实践中几乎没有存在感(Zig 的性能场景恰恰是要躲开 GC 的);
- 「引用计数」——概念本身真实(Python、Objective-C 的内存模型就是它), 但真实 Zig 里它只是一个你可以自己用结构体实现的模式, 标准库没有把它抬成「官方替代方案」;
- 书漏掉的第三条、也是最常用的一条:不解决,直接管好—— 分配器参数 + defer/errdefer + arena(第 8.3 节), 真实 Zig 社区的主流答案根本不是「找个自动机制」,而是「让手动变得可检查」。
判断(我们的,不是书里的): 7.4 节的「替代方案」清单暴露了书的价值观: 它默认「手动管理是麻烦,需要被自动机制拯救」——这是 GC 语言用户的视角。 真实 Zig 社区的价值观相反:手动管理是可控性,自动机制才是失去确定性。 同一个「无 GC」的事实,两种读法;书选了与 Zig 社区相反的那一种。 如果错,会错在: 对刚入门的应用开发者来说,「先找个 GC 顶着」 确实是降低门槛的合理路径——但这是人的权宜,不是语言的设计。
7. 边界与局限
- 书把「手动内存管理」讲成「malloc/free 别忘配对」,这是 C 水平; 真实 Zig 的水平是「分配器是类型系统的一部分参数,随函数签名流动」, 两者差距就是 C 和 Zig 这门语言的差距本身。
- 书没讲真实 Zig 检测泄漏的工具链(DebugAllocator 的 leak 报告、
std.testing.allocator在测试里抓泄漏)——这些才是 Zig 内存章的干货。 - 7.4 节说手动管理带来「确定性」(何时分配何时释放由你说了算)—— 这句判断本身是对的,也是实时系统的真实诉求(第 12 章会再用); 可惜整章只有这类「判断句」是真的,支撑判断的机制全是借的。
8. 逐条账目:书里的内存例子,走查到底
8.1 书 7.1 节的两个事故例子——概念层逐步走查
泄漏例(allocate_memory 函数申请 100 字节返回,调用处用完不释放)1:
① allocate_memory() 被调用,内部申请 100 字节,拿到这块内存的起始地址
② 函数返回,地址被存进 ptr——注意:能找回这块内存的,只有 ptr 这一个变量
③ main 继续跑,ptr 出了作用域(再也用不到这个名字)
④ 那 100 字节:没还。系统以为你还在用,不敢给别人
⑤ 循环这个模式一百万次:一百万个 100 字节 ≈ 95 MiB 蒸发
(100 字节 × 1,000,000 ÷ 1024² = 95.4 MiB,这个换算是按通用知识算的)
图说:泄漏的本质不是「忘了 free」这个动作,而是
「唯一能找回内存的线索(指针变量)丢了」。溢出例(dest 10 字节,src 24 字节,照 copy)1:
① dest 是一块 10 字节的缓冲(buffer,程序里用来暂存数据的一小段内存)
② src = "This is a very long string" 长 26 字节(24 个字符 + 引号外不算,
含空格与结尾,26 是逐个数出来的)
③ 照抄:第 1–10 字节进 dest;第 11–26 字节继续往 dest 后面的内存写
④ dest 后面是什么?不知道——可能是另一个变量的值,可能是返回地址
⑤ 真实 Zig(安全构建):第 11 个字节那一写,当场 panic,
报错精确到行号,而不是像 C 那样默默写完
图说:C 的恐怖在于第 ④ 步的「不知道」;Zig 用运行期检查
把「不知道」变成「现在就停」。8.2 书 7.3 节的 alloc 例子——逐行对回真实 API
书里那个「申请 100 字节 → 清零 → 释放」的三行代码6, 逐行翻译成真 Zig:
书里的行 真实 Zig 的写法
────────────────────────────────────────────────────────────
var ptr = std.alloc.malloc(100) var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const ptr = try allocator.alloc(u8, 100);
defer std.alloc.free(ptr) defer allocator.free(ptr);
std.mem.set(ptr, 0, 100) @memset(ptr, 0);
图说:书的版本 3 行,真实的版本多了三行——多出来的不是啰嗦,
是「分配器从哪来」这个 Zig 非要你回答的问题。这几行值得停在一个细节上:try allocator.alloc(u8, 100) 里的 try。
分配可能失败(内存不够),真实 Zig 逼你当场表态——要么 try
(失败就把错误交还给调用我的人),要么 catch(失败我就在这儿处理)。
书的整章没有出现一次「分配失败怎么办」,而这在真实 Zig 里是
每个分配点的必答题6。
8.3 真实 Zig 内存课缺的三节,补在这里
书没讲、但真实 Zig 内存章必讲的另外三件事:
- 泄漏是被工具抓住的,不是靠自觉。 真实 Zig 的通用分配器在 Debug 构建下会记录每一次申请和归还,程序退出时把「没还的」 连同申请它的调用栈一起打出来;测试里还有专门的 testing allocator, 一个测试漏了 free,测试结果直接红7。
- arena(竞技场分配器)是最常用的「省心模式」。 一块 arena 配好 之后,里面所有的分配不用一笔笔还——把整个 arena 一起倒掉就行 (典型场景:处理一个网络请求,请求结束,arena 整个释放)。 「一次性批量归还」这个设计把 defer 配对的手写量又砍了一半7。
- C 互操作是内存章的隐藏主角。 Zig 直接调 C 的 malloc/free 混用自己的分配器是日常操作(官方演示里 C 库的回调由 Zig 提供, 两边内存各管各的,互不越界)7。书的第七章对此一字未提—— 而「和 C 无缝对接」恰是第 01 章里书自己吹过的 Zig 卖点。
9. 可带走的
- 内存泄漏和缓冲区溢出两个概念是真的,后果描述可信;真实 Zig 越界即 panic,不是默默写穿。
- 书里 7.2 节是 Rust,不是 Zig:所有权、&mut 借用、<'a> 生命周期,Zig 一样都没有。
- Zig 的真实路线:编译期不禁止,运行期现形 + 释放点用 defer/errdefer 钉死 + 分配器当参数传。
std.alloc.malloc不存在;真实 API 是「先有一个分配器(如 std.heap 里的),调它的 alloc/free 方法」。- defer 是本章唯一必须带走的真机制;errdefer(出错才跑)书没讲,比 defer 更值。
- 分配器参数化设计是 Zig 内存管理的灵魂——它让同一份代码从服务器跑到裸机;书通篇没有这个概念。
- 真实 Zig 里每次分配都可能失败,必须 try 或 catch 当场表态——书整章没问过这个问题。
- 泄漏靠工具抓(DebugAllocator 报告、测试分配器),不靠自觉;arena 是最常用的省心模式。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 内存泄漏与溢出概念 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:9(搜「Memory Leaks」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:35(搜「Buffer Overflows」) |
| 所有权三规则 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:77(搜「Ownership refers to who has control」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:81(搜「following ownership rules」) |
| 可变/不可变借用 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:93(搜「Mutable Borrowing」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:119(搜「Immutable Borrowing」) |
| <'a> 生命周期标注 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:147(搜「Lifetime Annotations」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:155(搜「borrow_value<'a>」) |
| alloc 模块 API 表 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:167(搜「The alloc module provides」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:189(搜「std.alloc.malloc(100)」) |
| 无 GC 与手动管理 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:237(搜「does not have built-in garbage collection」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:265(搜「Performance: Manual memory management」) |
| defer 与释放配对 | Chapter 7: Memory Safety and Ownership | text/09-ch07-chapter-7-memory-safety-and-ownership.txt:357(搜「defer keyword」) · text/09-ch07-chapter-7-memory-safety-and-ownership.txt:359(搜「before a function returns」) |
Footnotes
-
出处:「Chapter 7: Memory Safety and Ownership」第 9–73 段(
text/09-ch07-chapter-7-memory-safety-and-ownership.txt:9,搜「Memory Leaks」;:35,搜「Buffer Overflows」;:59,搜「overwrite adjacent memory」)。概念与后果描述可信。 ↩ ↩2 ↩3 ↩4 ↩5 -
补充(不在书里,来自权威外部资料):Zig 官方概览的「Manual memory management」一节:Zig 程序员必须自己管理内存、必须处理分配失败;所有需要内存的 std 函数都显式接收 allocator 参数,标准库因此可用于 freestanding(无操作系统)目标;errdefer 示例(create 设备 + 分配 name + 失败回滚)。安全构建下越界即 panic 并带堆栈。来源:《Overview — Zig Programming Language》https://ziglang.org/learn/overview/(查阅于 2026-10-19)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
出处:「Chapter 7: Memory Safety and Ownership」第 75–163 段(
text/09-ch07-chapter-7-memory-safety-and-ownership.txt:77,搜「Ownership refers to who has control」;:83,搜「only have one owner」;:101,搜「&mut i32」;:127,搜「&i32」;:155,搜「borrow_value<'a>」)。所有权/借用/生命周期均为 Rust 机制,非 Zig。 ↩ ↩2 ↩3 -
出处:「Chapter 7: Memory Safety and Ownership」第 357–379 段(
text/09-ch07-chapter-7-memory-safety-and-ownership.txt:357,搜「defer keyword」;:359,搜「executed before a function returns」)。defer 语义真实;「even if an error occurs」「in case of exceptions」的表述借自带异常的语言——Zig 无异常。 ↩ ↩2 ↩3 -
出处:「Chapter 7: Memory Safety and Ownership」第 275–285 段(
text/09-ch07-chapter-7-memory-safety-and-ownership.txt:279,搜「third-party garbage collectors」)。「可集成第三方 GC」无任何具体方案或项目名支撑。 ↩ ↩2 -
出处:「Chapter 7: Memory Safety and Ownership」第 181–199 段(
text/09-ch07-chapter-7-memory-safety-and-ownership.txt:185,搜「const std = @import("std");」;:189,搜「std.alloc.malloc(100)」;:191,搜「defer std.alloc.free(ptr)」;:193,搜「std.mem.set(ptr, 0, 100)」)。 ↩ ↩2 -
补充(不在书里,来自权威外部资料):DebugAllocator 的泄漏报告机制;errdefer 的 create/destroy 配对示例;「Any functions that need to allocate memory accept an allocator parameter」。来源:《Overview — Zig Programming Language》https://ziglang.org/learn/overview/(查阅于 2026-10-19)。 ↩ ↩2 ↩3