Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

内存管理模型

Zig 没有垃圾回收器,没有借用检查器。内存管理的核心原则只有一条:需要分配内存的函数,显式接收 Allocator 参数。谁分配、谁拥有、谁释放——全部由开发者显式决定。

Allocator 接口

std.mem.Allocator 是所有分配器的统一接口,内部是 ptr + vtable(详见接口与设计模式)。核心方法:

方法用途用法
create(T)分配单个 T 并返回 *Tvar p = try a.create(i32)
destroy(ptr)释放 create 返回的指针a.destroy(p)
alloc(T, n)分配 nT 值的空间var buf = try a.alloc(u8, 64)
free(slice)释放 alloc 返回的切片a.free(buf)
realloc(s, n)调整已有切片的大小buf = try a.realloc(buf, 128)

allocfree 配对,createdestroy 配对——混用会导致未定义行为。跨分配器混用(如 Arena 分配后用 page_allocator 释放)同样是未定义行为:不同分配器的内部布局、元数据和释放策略各不相同,常见的后果包括运行时崩溃、堆损坏或静默泄漏。

realloc 用于调整已有切片的大小——分配器会在原地尝试扩容,空间不够时自动分配新内存并复制数据、释放旧区域:

所有权

转移所有权:函数分配,调用者释放

const std = @import("std");

fn createBuffer(allocator: std.mem.Allocator, len: usize) ![]u8 {
    const buffer = try allocator.alloc(u8, len);
    return buffer; // 所有权转移给调用者
}

test "transfer ownership" {
    const a = std.testing.allocator;
    const buf = try createBuffer(a, 128);
    defer a.free(buf);
}

createBuffer 分配并返回内存——调用者接收所有权,负责释放。

借用:只读视图不拥有

fn countSpaces(text: []const u8) usize {
    var count: usize = 0;
    for (text) |ch| {
        if (ch == ' ') count += 1;
    }
    return count;
}

[]const u8 表示「借用一段只读数据」,不拥有、不释放。

栈 vs 堆

栈变量随作用域自动回收,不需要 allocator:

var value: i32 = 42; // 栈上

堆内存通过 allocator 申请,生命周期可超出作用域,必须手动释放:

const a = std.testing.allocator;
const ptr = try a.create(i32); // 堆上
defer a.destroy(ptr);

能用栈时优先用栈——需要动态大小或跨作用域生命周期时再引入堆分配。

常用分配器

Zig 0.16 中,main 函数通过 std.process.Init 直接提供 gpaiogpa 实际上是一个 GeneralPurposeAllocator,内部组合了多种分配策略,适合作为程序的通用分配器:

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;
    // ...
}

std.testing.allocator

测试专用,自带泄漏检测。每个 test 块结束时自动检查,发现泄漏则测试失败:

test "testing allocator" {
    const a = std.testing.allocator;
    const buf = try a.alloc(u8, 32);
    defer a.free(buf);
}

std.heap.ArenaAllocator

批量分配,统一释放。适合阶段性对象(解析配置、构造 AST、单次请求):

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const a = arena.allocator();

const first = try a.alloc(u8, 10);
const second = try a.alloc(u8, 20);
// 不需要逐项 free——arena.deinit() 统一回收

0.16 中 ArenaAllocator 已是 lock-free 且线程安全,无需额外包装。代价是中间无法回收单个对象,arena 生命周期过长可能退化为慢性泄漏。

std.heap.FixedBufferAllocator

在已有内存块上分配,不向系统扩张。适合已知大小上限或嵌入式场景:

var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const a = fba.allocator();

const slice = try a.alloc(u8, 256); // 从 buf 中分配

超量分配直接报错——「宁可早点失败,不无限增长」。

std.heap.DebugAllocator

任何程序中启用泄漏检测和双重释放检测。非测试环境下最实用的调试工具:

var debug_alloc: std.heap.DebugAllocator(.{}) = .init;
const a = debug_alloc.allocator();

const buf = try a.alloc(u8, 1024);
a.free(buf);

_ = try a.alloc(u8, 512); // 故意不释放

const check = debug_alloc.deinit(); // 打印泄漏回溯到 stderr
if (check == .leak) @panic("memory leak");

std.testing.allocator 底层就是 DebugAllocator

std.heap.page_allocator

直接向操作系统申请内存,常用作 Arena 或 DebugAllocator 的后端:

const a = std.heap.page_allocator;
const buf = try a.alloc(u8, 4096);
defer a.free(buf);

资源清理:defererrdefer

正常路径

const buf = try a.alloc(u8, 64);
defer a.free(buf);
// 函数结束时 buf 自动释放

半初始化资源清理

多次分配时,任何一次失败都必须回收已分配的资源。errdefer 只在函数以错误返回时执行:

fn makePair(a: std.mem.Allocator) !struct { x: []u8, y: []u8 } {
    const x = try a.alloc(u8, 8);
    errdefer a.free(x); // 后续失败时回收 x

    const y = try a.alloc(u8, 16);
    errdefer a.free(y); // 后续失败时回收 y

    return .{ .x = x, .y = y };
}

test "makePair" {
    const a = std.testing.allocator;
    const pair = try makePair(a);
    defer a.free(pair.x);
    defer a.free(pair.y);
}

模式:每次成功分配后紧跟 errdefer。成功返回时 errdefer 不执行,失败时按 LIFO 逆序回收。多个 defer / errdefer 混用时,执行顺序也是 LIFO——最后注册的最先执行。

实践:如何保证 Allocator 使用正确

跨分配器混用的根因通常是生命周期不清。掌握 defer/errdefer 之后,以下是保证 Allocator 使用正确的几种实用模式。

始终在同一函数内分配并释放——分配和释放距离越近,越难出错。defer 天然支撑这个模式:

fn process(allocator: std.mem.Allocator) !void {
    const buf = try allocator.alloc(u8, 64);
    defer allocator.free(buf);
}

批量对象用 Arena——整个阶段用一个 Arena 分配,只需一次 deinit,不存在「该用哪个 allocator 释放」的问题。解析器、请求处理器、构建 AST 都是典型场景:

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const a = arena.allocator();

const config = try parseConfig(a, path);   // 内部可能频繁 alloc
const report = try generateReport(a, config);
// defer arena.deinit() 统一回收,无需逐项 free

转移所有权时从函数名上区分——dupetoOwnedcreate 等前缀表明返回的是新分配的数据,调用者负责释放;trimeqlcount 不分配:

const owned = try allocator.dupe(u8, slice);  // 调用者必须 free
const view = std.mem.trim(u8, slice, " ");    // 借用视图,无须释放

测试用 std.testing.allocator——每个 test 结束自动扫描泄漏,不正确的 allocator 使用直接测试失败。

这些模式的核心思路相同:让 allocator 的责任边界在代码中是可见的——分配和释放在同一作用域、用 Arena 减少释放点、用命名传达所有权。

0.16 容器与 Allocator

0.16 中 ArrayList 和大部分容器采用非托管设计——内部不存 allocator,每个需要分配的方法都显式传入:

test "ArrayList in 0.16" {
    const a = std.testing.allocator;
    var list: std.ArrayList(u32) = .empty;       // 零初始化,无需 allocator
    defer list.deinit(a);                         // 释放时传入

    try list.append(a, 42);                       // 每个操作传 allocator
    try list.appendSlice(a, &.{ 10, 20, 30 });

    try std.testing.expectEqual(@as(usize, 4), list.items.len);
}

这个设计与 allocator 显式传递一脉相承:容器的内存策略完全由调用方控制,不会有隐藏分配。

HashMap 系列中,AutoHashMapStringHashMap 仍是托管类型(结构体内存 allocator),这是为了保持 key/value 查询 API 的简洁性。详见常用标准库模块详解

要点

  • 显式传递 allocator——不依赖全局状态,分配策略由调用方决定
  • 所有权清楚——函数分配 → 调用者释放;接收 []const T → 只借用
  • errdefer 是安全网——半初始化资源必须回收
  • 能用栈先用栈——堆分配引入生命周期和释放责任
  • 选对分配器——测试用 testing.allocator,批量对象用 Arena,调试用 DebugAllocator
  • 0.16 容器不存 allocator——.empty 初始化,方法传 allocator

相关阅读接口与设计模式指针、切片与对齐常用标准库模块详解