编程语言研究书库

内存安全与所谓所有权——把 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 的所有权规则是:

  1. 「一个值同时只能有一个所有者」;
  2. 「所有者离开作用域,值被自动 drop(释放)」;
  3. 「值可以被借用(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 内存章必讲的另外三件事:

  1. 泄漏是被工具抓住的,不是靠自觉。 真实 Zig 的通用分配器在 Debug 构建下会记录每一次申请和归还,程序退出时把「没还的」 连同申请它的调用栈一起打出来;测试里还有专门的 testing allocator, 一个测试漏了 free,测试结果直接红7。
  2. arena(竞技场分配器)是最常用的「省心模式」。 一块 arena 配好 之后,里面所有的分配不用一笔笔还——把整个 arena 一起倒掉就行 (典型场景:处理一个网络请求,请求结束,arena 整个释放)。 「一次性批量归还」这个设计把 defer 配对的手写量又砍了一半7。
  3. C 互操作是内存章的隐藏主角。 Zig 直接调 C 的 malloc/free 混用自己的分配器是日常操作(官方演示里 C 库的回调由 Zig 提供, 两边内存各管各的,互不越界)7。书的第七章对此一字未提—— 而「和 C 无缝对接」恰是第 01 章里书自己吹过的 Zig 卖点。

9. 可带走的

  1. 内存泄漏和缓冲区溢出两个概念是真的,后果描述可信;真实 Zig 越界即 panic,不是默默写穿。
  2. 书里 7.2 节是 Rust,不是 Zig:所有权、&mut 借用、<'a> 生命周期,Zig 一样都没有。
  3. Zig 的真实路线:编译期不禁止,运行期现形 + 释放点用 defer/errdefer 钉死 + 分配器当参数传。
  4. std.alloc.malloc 不存在;真实 API 是「先有一个分配器(如 std.heap 里的),调它的 alloc/free 方法」。
  5. defer 是本章唯一必须带走的真机制;errdefer(出错才跑)书没讲,比 defer 更值。
  6. 分配器参数化设计是 Zig 内存管理的灵魂——它让同一份代码从服务器跑到裸机;书通篇没有这个概念。
  7. 真实 Zig 里每次分配都可能失败,必须 try 或 catch 当场表态——书整章没问过这个问题。
  8. 泄漏靠工具抓(DebugAllocator 报告、测试分配器),不靠自觉;arena 是最常用的省心模式。

10. 原文地图

主题原书章原文位置
内存泄漏与溢出概念Chapter 7: Memory Safety and Ownershiptext/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 Ownershiptext/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 Ownershiptext/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 Ownershiptext/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 Ownershiptext/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 Ownershiptext/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 Ownershiptext/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

  1. 出处:「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

  2. 补充(不在书里,来自权威外部资料):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

  3. 出处:「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

  4. 出处:「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

  5. 出处:「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

  6. 出处:「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

  7. 补充(不在书里,来自权威外部资料):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

On this page