概述
Zig处理动态内存的方法是显式的、可组合的和可测试的。API不通过隐式全局变量隐藏分配,而是接受一个std.mem.Allocator并明确地将所有权返回给其调用者。本章展示了核心分配器接口(alloc、free、realloc、resize、create、destroy),介绍了最常见的分配器实现(页分配器、带泄漏检测的Debug/GPA、arenas和固定缓冲区),并建立了通过您自己的API传递分配器的模式(参见Allocator.zig和heap.zig)。
你将学习何时优先选择批量释放的arena,如何使用固定的栈缓冲区来消除堆流量,以及如何安全地增长和收缩分配。这些技能支撑着本书的其余部分——从集合到I/O适配器——并将使后续项目更快、更健壮(参见03)。
学习目标
- 使用
std.mem.Allocator来分配、释放和调整类型化切片和单个项目的大小。 - 选择一个分配器:页分配器、Debug/GPA(泄漏检测)、arena、固定缓冲区或栈回退组合。
- 设计接受分配器并将所有权内存返回给调用者的函数(参见08)。
分配器接口
Zig的分配器是一个小型的、值类型的接口,具有用于类型化分配和显式释放的方法。包装器处理哨兵和对齐,因此您大多数时候可以停留在[]T级别。
alloc/free、create/destroy和哨兵
基础知识:分配一个类型化的切片,改变其元素,然后释放。对于单个项目,优先使用create/destroy。当需要一个用于C互操作的空终止符时,使用allocSentinel(或dupeZ)。
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator; // OS-backed; fast & simple
// 操作系统支持;快速且简单
// Allocate a small buffer and fill it.
// 分配一个小缓冲区并填充它。
const buf = try allocator.alloc(u8, 5);
defer allocator.free(buf);
for (buf, 0..) |*b, i| b.* = 'a' + @as(u8, @intCast(i));
std.debug.print("buf: {s}\n", .{buf});
// Create/destroy a single item.
// 创建/销毁单个项。
const Point = struct { x: i32, y: i32 };
const p = try allocator.create(Point);
defer allocator.destroy(p);
p.* = .{ .x = 7, .y = -3 };
std.debug.print("point: (x={}, y={})\n", .{ p.x, p.y });
// Allocate a null-terminated string (sentinel). Great for C APIs.
// 分配空终止字符串(哨兵)。非常适合C API。
var hello = try allocator.allocSentinel(u8, 5, 0);
defer allocator.free(hello);
@memcpy(hello[0..5], "hello");
std.debug.print("zstr: {s}\n", .{hello});
}
$ zig run alloc_free_basics.zigbuf: abcde
point: (x=7, y=-3)
zstr: hello优先使用{s}来打印[]const u8切片(不需要终止符)。当与需要尾随\0的API互操作时,使用allocSentinel或dupeZ。
分配器接口的底层工作原理
std.mem.Allocator类型是一个使用指针和虚函数表(vtable)的类型擦除接口。这种设计允许任何分配器实现通过同一个接口传递,从而在不为常见情况带来虚拟分派开销的情况下实现运行时多态。
虚函数表包含四个基本操作:
- alloc:返回一个指向具有指定对齐方式的
len字节的指针,如果失败则返回错误 - resize:尝试在原地扩展或收缩内存,返回
bool - remap:尝试扩展或收缩内存,允许重定位(由
realloc使用) - free:释放并使内存区域无效
高级API(create、destroy、alloc、free、realloc)用类型安全、符合人体工程学的方法包装了这些虚函数表函数。这种两层设计使分配器实现保持简单,同时为用户提供方便的类型化分配(参见Allocator.zig)。
Debug/GPA和Arena分配器
对于整个程序的工作,Debug/GPA是默认选择:它跟踪分配并在deinit()时报告泄漏。对于作用域内的、临时的分配,arena在deinit()期间一次性返回所有内容。
const std = @import("std");
pub fn main() !void {
// GeneralPurposeAllocator with leak detection on deinit.
// 在deinit时具有泄漏检测功能的GeneralPurposeAllocator。
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer {
const leaked = gpa.deinit() == .leak;
if (leaked) @panic("leak detected");
}
const alloc = gpa.allocator();
const nums = try alloc.alloc(u64, 4);
defer alloc.free(nums);
for (nums, 0..) |*n, i| n.* = @as(u64, i + 1);
var sum: u64 = 0;
for (nums) |n| sum += n;
std.debug.print("gpa sum: {}\n", .{sum});
// Arena allocator: bulk free with deinit.
var arena_inst = std.heap.ArenaAllocator.init(alloc);
defer arena_inst.deinit();
const arena = arena_inst.allocator();
const msg = try arena.dupe(u8, "temporary allocations live here");
std.debug.print("arena msg len: {}\n", .{msg.len});
}
$ zig run gpa_arena.ziggpa sum: 10
arena msg len: 31在Zig 0.15.x中,std.heap.GeneralPurposeAllocator是Debug分配器的一个薄别名。始终检查deinit()的返回值:.leak表示有东西未被释放。
选择和组合分配器
分配器是常规值:你可以传递它们、包装它们、组合它们。两个主力工具是固定缓冲区分配器(用于栈支持的突发分配)和用于动态增长和收缩的realloc/resize。
为安全和调试包装分配器
因为分配器只是具有通用接口的值,所以你可以包装一个分配器以添加功能。std.mem.validationWrap函数通过在委托给底层分配器之前添加安全检查来演示这种模式。
ValidationAllocator包装器验证:
- 分配大小大于零
- 返回的指针具有正确的对齐方式
- 在resize/free操作中内存长度是有效的
这种模式非常强大:你可以构建自定义的分配器包装器,添加日志记录、度量收集、内存限制或其他横切关注点,而无需修改底层分配器。包装器在执行其检查或副作用后,简单地委托给underlying_allocator。mem.zig
栈上的固定缓冲区
使用FixedBufferAllocator从栈数组中获得快速、零系统调用的分配。当你用完时,你会得到error.OutOfMemory——这正是你需要回退或修剪输入的信号。
const std = @import("std");
pub fn main() !void {
var backing: [32]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&backing);
const A = fba.allocator();
// 3 small allocations should fit.
// 3个小分配应该能容纳。
const a = try A.alloc(u8, 8);
const b = try A.alloc(u8, 8);
const c = try A.alloc(u8, 8);
_ = a;
_ = b;
_ = c;
// This one should fail (32 total capacity, 24 already used).
// 这个应该失败(总容量32,已使用24)。
if (A.alloc(u8, 16)) |_| {
std.debug.print("unexpected success\n", .{});
} else |err| switch (err) {
error.OutOfMemory => std.debug.print("fixed buffer OOM as expected\n", .{}),
else => return err,
}
}
$ zig run fixed_buffer.zigfixed buffer OOM as expected为了优雅地回退,用std.heap.stackFallback(N, fallback)在较慢的分配器上组合一个固定缓冲区。返回的对象有一个.get()方法,每次都产生一个新的Allocator。
用realloc/resize安全地增长和收缩
realloc返回一个新的切片(并且可能移动分配)。resize尝试在原地更改长度并返回bool;当它成功时,请记住也要更新你的切片的len。
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer { _ = gpa.deinit(); }
const alloc = gpa.allocator();
var buf = try alloc.alloc(u8, 4);
defer alloc.free(buf);
for (buf, 0..) |*b, i| b.* = 'A' + @as(u8, @intCast(i));
std.debug.print("len={} contents={s}\n", .{ buf.len, buf });
// Grow using realloc (may move).
// 使用realloc增长(可能会移动)。
buf = try alloc.realloc(buf, 8);
for (buf[4..], 0..) |*b, i| b.* = 'a' + @as(u8, @intCast(i));
std.debug.print("grown len={} contents={s}\n", .{ buf.len, buf });
// Shrink in-place using resize; remember to slice.
// 使用resize就地缩小;记住要切片。
if (alloc.resize(buf, 3)) {
buf = buf[0..3];
std.debug.print("shrunk len={} contents={s}\n", .{ buf.len, buf });
} else {
// Fallback when in-place shrink not supported by allocator.
// 当分配器不支持就地缩小时的后备方案。
buf = try alloc.realloc(buf, 3);
std.debug.print("shrunk (realloc) len={} contents={s}\n", .{ buf.len, buf });
}
}
$ zig run resize_and_realloc.ziglen=4 contents=ABCD
grown len=8 contents=ABCDabcd
shrunk (realloc) len=3 contents=ABC在resize(buf, n) == true之后,旧的buf仍然有其先前的len。重新切片它(buf = buf[0..n]),这样下游代码才能看到新的长度。
对齐如何工作的底层原理
Zig的内存系统使用紧凑的2的幂对齐表示。 std.mem.Alignment枚举将对齐存储为log₂值,从而实现高效存储,同时提供丰富的实用方法。
这种紧凑的表示提供了用于以下目的的实用方法:
- 与字节单位之间转换:
@"16".toByteUnits()返回16,fromByteUnits(16)返回@"16" - 向前对齐地址:
forward(addr)向上舍入到下一个对齐的边界 - 向后对齐地址:
backward(addr)向下舍入到上一个对齐的边界 - 检查对齐:
check(addr)如果地址满足对齐要求,则返回true - 类型对齐:
of(T)返回类型T的对齐方式
当您看到alignedAlloc(T, .@"16", n)或在自定义分配器中使用对齐时,您正在使用这个log₂表示。紧凑的存储允许Zig有效地跟踪对齐,而不会浪费空间(参见mem.zig)。
分配器作为参数的模式
你的API应该接受一个分配器,并将拥有的内存返回给调用者。这使得生命周期明确,并让你的用户为他们的上下文选择正确的分配器(用于临时的arena、通用的GPA、可用的固定缓冲区)。
const std = @import("std");
fn joinSep(allocator: std.mem.Allocator, parts: []const []const u8, sep: []const u8) ![]u8 {
var total: usize = 0;
for (parts) |p| total += p.len;
if (parts.len > 0) total += sep.len * (parts.len - 1);
var out = try allocator.alloc(u8, total);
var i: usize = 0;
for (parts, 0..) |p, idx| {
@memcpy(out[i .. i + p.len], p);
i += p.len;
if (idx + 1 < parts.len) {
@memcpy(out[i .. i + sep.len], sep);
i += sep.len;
}
}
return out;
}
pub fn main() !void {
// Use GPA to build a string, then free.
// 使用GPA构建字符串,然后释放。
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer { _ = gpa.deinit(); }
const A = gpa.allocator();
const joined = try joinSep(A, &.{ "zig", "likes", "allocators" }, "-");
defer A.free(joined);
std.debug.print("gpa: {s}\n", .{joined});
// Try with a tiny fixed buffer to demonstrate OOM.
// 尝试使用微小的固定缓冲区来演示OOM。
var buf: [8]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const B = fba.allocator();
if (joinSep(B, &.{ "this", "is", "too", "big" }, ",")) |s| {
// If it somehow fits, free it (unlikely with 16 bytes here).
// 如果万一能容纳,就释放它(这里16字节不太可能)。
B.free(s);
std.debug.print("fba unexpectedly succeeded\n", .{});
} else |err| switch (err) {
error.OutOfMemory => std.debug.print("fba: OOM as expected\n", .{}),
else => return err,
}
}
$ zig run allocator_parameter.ziggpa: zig-likes-allocators
fba: OOM as expected返回[]u8(或[]T)将所有权干净地转移给调用者;请在文档中说明调用者必须free。如果可以,提供一个comptime友好的变体,该变体写入调用者提供的缓冲区。04
注意与警告
- 释放你所分配的。在本书中,示例在成功
alloc后立即使用defer allocator.free(buf)。 - 收缩:对于原地收缩,优先使用
resize;如果它返回false,则回退到realloc。 - Arenas:永远不要将arena拥有的内存返回给长生命周期的调用者。Arena内存在
deinit()时死亡。 - GPA/Debug:检查
deinit()并将泄漏检测与std.testing连接到测试中(参见testing.zig)。 - 固定缓冲区:非常适合有界的工作负载;与
stackFallback结合以优雅地降级。
练习
- 实现
splitJoin(allocator, s: []const u8, needle: u8) ![]u8,该函数在一个字节上分割并用'-'重新连接。添加一个写入调用者缓冲区的变体。 - 重写你之前的一个CLI工具,使其从
main接受一个分配器并将其贯穿。尝试使用ArenaAllocator来处理临时缓冲区。06 - 用
stackFallback包装FixedBufferAllocator,并展示相同函数如何在小输入上成功,但在较大输入上回退。
替代方案和边缘情况
- 对齐敏感的分配:使用
alignedAlloc(T, .@"16", n)或传播对齐的类型化助手。 - 接口支持零大小类型和零长度切片;不要对它们进行特殊处理。
- C互操作:当链接libc时,考虑使用
c_allocator/raw_c_allocator来匹配外部的分配语义;否则优先使用页分配器/GPA。