乐于分享
好东西不私藏

Zig 源码告诉你什么是真正的手动内存自由(第10篇)

Zig 源码告诉你什么是真正的手动内存自由(第10篇)

在很多现代语言里,“内存管理”要么被自动藏起来(GC),要么被借用检查器严格约束(Rust)。而 Zig 把选择权完完全全交还给了程序员,却没有让这件事变得痛苦。

这期我们直接看 Zig 标准库源码,看看它是怎么用最朴素、最显式的方式实现“手动内存自由”的,同时还能写出可读性高、泄漏可检测、性能可控的代码。

1. Zig 从不偷偷给你分配内存

这是 Zig 内存哲学最核心的一条:标准库里凡是可能分配内存的函数,都必须显式接收一个 std.mem.Allocator 参数

看几个典型 std 源码例子(基于 master 分支,路径大致为 lib/std/…):

// std/ArrayList.zig (简化版核心结构)
pub fn ArrayList(comptime T: type) type {
    return struct {
        items: []T = &[_]T{},
        capacity: usize = 0,
        allocator: Allocator,

        pub fn init(allocator: Allocator) @This() { ... }

        pub fn deinit(self: *@This()) void {
            self.allocator.free(self.items);
        }

        pub fn append(self: *@This(), item: T) !void {
            if (self.items.len == self.capacity) {
                // 一定会看到 grow 操作 → 分配新内存
                try self.ensureTotalCapacityPrecise(self.items.len + 1);
            }
            self.items[self.items.len] = item;
            self.items.len += 1;
        }
    };
}

关键点:

  • 没有全局 allocator
  • 没有隐式堆分配
  • 想用 ArrayList,必须自己传 allocator
  • 想销毁,必须显式调用 deinit()

这和很多语言的 vectorlist 形成了鲜明对比——它们往往偷偷用线程局部的默认堆。

2. Allocator 接口本身极简,却能力极强

看最核心的定义(lib/std/mem/Allocator.zig):

pub const Allocator = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        alloc: *const fn (
            ctx: *anyopaque,
            len: usize,
            ptr_align: u8,
            ret_addr: usize,
        ) ?[*]u8,

        resize: *const fn (
            ctx: *anyopaque,
            buf: []u8,
            buf_align: u8,
            new_len: usize,
            ret_addr: usize,
        ) bool,

        free: *const fn (
            ctx: *anyopaque,
            buf: []u8,
            buf_align: u8,
            ret_addr: usize,
        ) void,
    };

    // 便利方法
    pub fn alloc(self: Allocator, comptime T: type, n: usize) Error![]T { ... }
    pub fn create(self: Allocator, comptime T: type) Error!*T { ... }
    pub fn free(self: Allocator, memory: anytype) void { ... }
    ...
};

只有三个虚函数:alloc / resize / free
却能实现几乎所有内存策略:

  • page_allocator(直接系统调用 mmap/malloc)
  • ArenaAllocator(只分配,最后一次性释放)
  • FixedBufferAllocator(栈上/静态缓冲区)
  • GeneralPurposeAllocator(带泄漏检测、双端链表、哨兵字节)
  • DebugAllocator(更强的边界检查、毒化内存)
  • LogAllocator、ThreadSafe、CAllocator、……

你甚至可以几行代码自己写一个符合这个 vtable 的自定义分配器。

3. defer + errdefer 让手动释放不再痛苦

Zig 不靠 RAII,而是靠更轻量的 defer

pub fn jsonParse(comptime T: type, allocator: Allocator, source: []const u8) !T {
    var parser = std.json.Parser.init(allocator, .alloc_if_needed);
    defer parser.deinit();

    var tree = try parser.parse(source);
    defer tree.deinit();

    return std.json.parseFromTokenSource(T, &parser, tree.root, .{});
}

哪怕中间有十几层嵌套错误返回,defer 也会在函数退出时按逆序执行清理。
这比 C 的 goto 清理模式清晰得多,比 RAII 类更直观(你一眼就能看到资源在哪释放)。

4. 真正“自由”的例子:ArenaAllocator 的使用模式

最能体现 Zig 内存自由的写法之一就是 Arena + 一级分配器:

pub fn doComplexWork(allocator: Allocator) !void {
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();                     // ← 一句释放全部

    const aa = arena.allocator();             // 子分配器

    var list = std.ArrayList(u32).init(aa);
    var map  = std.StringHashMap([]const u8).init(aa);
    var tree = std.json.ValueTree.init(aa);   // 随便用多少层嵌套结构

    // 中间几百行代码,疯狂 append、put、parse……
    // 全都不用单独 free!

    // 函数结束 → arena.deinit() → 全部归还
}

这种模式在:

  • 编译器 / 解释器的一次请求处理
  • 游戏的一帧逻辑
  • 命令行工具的整个生命周期
  • 测试用例

……里极其常见。它把“手动内存”变成了“作用域内存”,既安全又高效。

5. 标准库自带的泄漏检测有多狠?

把下面代码编译运行:

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);  // ← 这里会检测

    const alloc = gpa.allocator();

    _ = try alloc.alloc(u8, 4096);  // 故意不 free
}

你会看到类似输出:

error(gpa): memory address 0x... leaked:
C:\...\main.zig:XX:36 in main (exe)
    _ = try alloc.alloc(u8, 4096);
           ^

这是在编译期就能选开/关的检测机制,而不是运行时开销很大的 valgrind。

总结:Zig 给你的内存自由长什么样?

  1. 完全显式:没有默认 allocator,没有隐式分配
  2. 完全可换:同一个接口支持十几种策略
  3. 完全可检:DebugAllocator / GeneralPurposeAllocator 能抓泄漏、越界
  4. 语法友好:defer + errdefer + Arena 让手动管理写起来像自动管理一样顺
  5. 零成本抽象:你选 page_allocator 就是接近 malloc 的性能,你选 Arena 就是接近栈分配的性能

真正的内存自由,不是“随便写随便用”,而是我知道每一块内存从哪来,到哪去,我能精确控制它,我也能轻松验证它

欢迎关注「Zig 源码学习」系列,第11篇见~

(欢迎留言你最喜欢的 Zig 内存使用模式,或者你写过的最爽的一次 Arena 使用经历)

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Zig 源码告诉你什么是真正的手动内存自由(第10篇)

猜你喜欢

  • 暂无文章