Chapter 03

内存管理:Allocator 接口设计

理解 Zig 独特的 Allocator 接口,掌握显式内存管理的最佳实践

Zig 内存管理的哲学

为什么不用 GC?

Zig 坚持手动内存管理,原因在于其核心目标是系统编程和性能敏感场景。垃圾回收器(GC)带来以下问题:

Allocator 接口的核心思想

Zig 的内存管理哲学是:任何需要堆内存分配的函数,都必须通过参数接受一个 Allocator。这意味着:

// 标准库中的函数签名示例:ArrayList 接受 Allocator 参数
fn init(allocator: std.mem.Allocator) ArrayList(T) {
    return .{ .allocator = allocator };
}

// 你的函数如果需要分配内存,也应该接受 Allocator
fn buildMessage(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
    return std.fmt.allocPrint(allocator, "Hello, {s}!", .{name});
    // 调用者负责释放返回的内存
}

std.mem.Allocator 接口

接口方法

alloc(T, n)
分配 n 个 T 类型元素的切片([]T)。返回 ![]T,可能失败(OutOfMemory)。
create(T)
分配单个 T 类型值,返回 !*T(指向新分配内存的指针)。
free(slice)
释放 alloc 分配的切片内存。
destroy(ptr)
释放 create 分配的单个对象内存。
realloc(old, new_n)
重新调整切片大小,可能移动内存位置。
dupe(T, slice)
复制切片内容,分配新内存返回副本。

常用分配器

GeneralPurposeAllocator:通用分配器

std.heap.GeneralPurposeAllocator 是功能最完整的分配器,在 debug 模式下会检测内存泄漏、double free 和 use-after-free 错误。

const std = @import("std");

pub fn main() !void {
    // 声明 GPA(通常放在 main 函数开头)
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        // deinit 检查是否有内存泄漏,返回 .leak 或 .ok
        const check = gpa.deinit();
        if (check == .leak) std.debug.print("内存泄漏!\n", .{});
    }
    const allocator = gpa.allocator();

    // 分配整数数组
    const buf = try allocator.alloc(u8, 1024);
    defer allocator.free(buf);  // defer 确保退出时释放

    // 分配单个结构体
    const Point = struct { x: f32, y: f32 };
    const pt = try allocator.create(Point);
    defer allocator.destroy(pt);
    pt.* = .{ .x = 1.0, .y = 2.0 };
    std.debug.print("Point: ({d}, {d})\n", .{ pt.x, pt.y });
}

ArenaAllocator:竞技场分配器

ArenaAllocator 将所有分配统一管理,一次性释放全部内存。非常适合生命周期一致的数据(如处理一个 HTTP 请求、解析一个文件)。

pub fn processRequest(base_allocator: std.mem.Allocator) !void {
    // Arena 基于另一个分配器创建
    var arena = std.heap.ArenaAllocator.init(base_allocator);
    defer arena.deinit();  // 一次性释放所有分配

    const alloc = arena.allocator();

    // 所有这些分配在 arena.deinit() 时一起释放,无需逐一 free
    const name = try alloc.dupe(u8, "Alice");
    const greeting = try std.fmt.allocPrint(alloc, "Hello, {s}!", .{name});
    const response = try std.fmt.allocPrint(alloc, "Response: {s}", .{greeting});

    std.debug.print("{s}\n", .{response});
    // 函数结束时 defer 触发,arena 清空所有内存
}

FixedBufferAllocator:固定缓冲区分配器

从栈上的固定缓冲区分配内存,零堆分配。适合嵌入式系统或已知最大内存需求的场景。

pub fn embeddedExample() !void {
    // 在栈上分配 4KB 缓冲区
    var backing_buf: [4096]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&backing_buf);
    const alloc = fba.allocator();

    const slice = try alloc.alloc(u8, 100);
    _ = slice;
    // 超过 4096 字节会返回 OutOfMemory 错误,不会崩溃
    // 适合无堆内存的嵌入式环境
}

page_allocator 与 c_allocator

// page_allocator:直接向 OS 申请内存页,没有元数据开销
// 适合大块长期分配(如 mmap)
const buf = try std.heap.page_allocator.alloc(u8, 65536);
defer std.heap.page_allocator.free(buf);

// c_allocator:包装 libc 的 malloc/free,与 C 代码互操作时使用
const c_buf = try std.heap.c_allocator.alloc(u8, 256);
defer std.heap.c_allocator.free(c_buf);

defer:资源释放的利器

defer 的工作原理

defer 语句会在当前作用域结束时执行,无论是正常退出还是错误退出。多个 defer 按逆序(LIFO)执行,就像栈一样。

fn example() !void {
    std.debug.print("开始\n", .{});
    defer std.debug.print("defer 1\n", .{});
    defer std.debug.print("defer 2\n", .{});
    std.debug.print("结束\n", .{});
    // 输出顺序:开始 → 结束 → defer 2 → defer 1
}

// 常用模式:init + defer deinit
fn withFile() !void {
    const file = try std.fs.cwd().openFile("data.txt", .{});
    defer file.close();  // 无论后续是否出错,都会关闭文件

    var buf: [1024]u8 = undefined;
    const n = try file.read(&buf);
    std.debug.print("读取 {d} 字节\n", .{n});
}

为什么 Zig 不像 Rust 那样强制所有权?

Rust 的借用检查器在编译期强制所有权规则,任何违规都无法编译。Zig 的设计哲学不同:

Zig 的内存安全策略

这意味着 Zig 不能在编译期 100% 保证内存安全,但通过工具和纪律达到实际可用的安全级别。

所有权约定

// 约定 1:函数返回的切片由调用者负责释放
fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();
    return file.readToEndAlloc(allocator, 1024 * 1024);
    // 调用者必须 free 这个切片!
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    const content = try readFile(alloc, "data.txt");
    defer alloc.free(content);  // 不忘 free!
    std.debug.print("文件大小: {d}\n", .{content.len});
}

自定义 Allocator:实现日志分配器

Zig 的 Allocator 是一个接口(虚表),任何实现了 alloc/resize/free 方法的类型都可以作为 Allocator。这让你可以创建包装分配器,添加日志、统计等功能:

const std = @import("std");

/// 日志分配器:记录每次分配/释放
const LoggingAllocator = struct {
    backing_allocator: std.mem.Allocator,
    alloc_count: usize = 0,
    total_bytes: usize = 0,

    pub fn allocator(self: *@This()) std.mem.Allocator {
        return .{
            .ptr = self,
            .vtable = &.{
                .alloc = alloc,
                .resize = resize,
                .free = free,
            },
        };
    }

    // alloc 实现:委托给 backing,并记录统计
    fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
        const self: *LoggingAllocator = @ptrCast(@alignCast(ctx));
        const result = self.backing_allocator.vtable.alloc(
            self.backing_allocator.ptr, len, ptr_align, ret_addr
        );
        if (result != null) {
            self.alloc_count += 1;
            self.total_bytes += len;
            std.debug.print("[ALLOC] {} bytes (total: {} allocs)\n",
                .{ len, self.alloc_count });
        }
        return result;
    }

    fn resize(...) bool { ... }
    fn free(...) void { ... }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var logger = LoggingAllocator{ .backing_allocator = gpa.allocator() };
    const alloc = logger.allocator();

    const buf = try alloc.alloc(u8, 256);  // 输出: [ALLOC] 256 bytes (total: 1 allocs)
    defer alloc.free(buf);
}

ArrayList 与动态数组

std.ArrayList(T) 是 Zig 标准库提供的动态数组,类似 C++ 的 vector 或 Rust 的 Vec:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    // 创建动态数组
    var list = std.ArrayList(i32).init(alloc);
    defer list.deinit();  // 释放内部缓冲区

    // 追加元素(内部自动 2x 扩容)
    try list.append(10);
    try list.append(20);
    try list.append(30);

    // 插入到特定位置
    try list.insert(1, 15);  // [10, 15, 20, 30]

    // 获取切片视图
    const slice = list.items;
    std.debug.print("列表: {any}\n", .{slice});

    // 预分配容量避免多次重新分配
    try list.ensureTotalCapacity(100);
    std.debug.print("容量: {d}\n", .{list.capacity});

    // 移除末尾元素(返回被移除的值)
    const last = list.pop();
    std.debug.print("弹出: {d}\n", .{last});
}

StringHashMap:字符串键哈希表

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    // StringHashMap(V):键为字符串切片,值类型为 V
    var map = std.StringHashMap(u32).init(alloc);
    defer map.deinit();

    // 插入键值对
    try map.put("alice", 90);
    try map.put("bob", 85);
    try map.put("charlie", 92);

    // 查找(返回 optional)
    if (map.get("alice")) |score| {
        std.debug.print("alice 的分数: {d}\n", .{score});
    }

    // 检查是否存在
    std.debug.print("存在 bob: {}\n", .{map.contains("bob")});

    // 遍历所有键值对
    var iter = map.iterator();
    while (iter.next()) |entry| {
        std.debug.print("{s}: {d}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
    }

    // 使用 getOrPut 原子地获取或插入
    const result = try map.getOrPut("diana");
    if (!result.found_existing) {
        result.value_ptr.* = 88;  // 仅在新插入时设置值
    }
}

分配器选择指南

分配器适用场景特点
GeneralPurposeAllocator通用场景、测试Debug 模式检测泄漏,生产可用
ArenaAllocatorHTTP 请求、解析、批处理批量释放,零释放开销
FixedBufferAllocator嵌入式、栈上小对象零堆分配,超出报错不崩溃
page_allocator大块长期内存直接 mmap,页对齐
c_allocator与 C 代码互操作包装 malloc/free
自定义分配器监控、调试、特殊策略实现 Allocator 接口即可

本章小结

本章核心要点