Allocators
Zig has no global allocator and no garbage collector. Anything that needs heap
memory takes an std.mem.Allocator parameter, which means you can always see,
from the signature alone, whether a function allocates.
const std = @import("std");
const expect = std.testing.expect;
test "allocate a slice" {
const allocator = std.testing.allocator;
const numbers = try allocator.alloc(u32, 10);
defer allocator.free(numbers);
for (numbers, 0..) |*n, i| n.* = @intCast(i * i);
try expect(numbers[9] == 81);
}
test "arena frees everything at once" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// No individual `free` needed; `deinit` reclaims it all.
for (0..100) |_| _ = try allocator.alloc(u8, 64);
}
test "fixed buffer needs no heap at all" {
var buf: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const allocator = fba.allocator();
const slice = try allocator.alloc(u8, 100);
try expect(slice.len == 100);
try expect(allocator.alloc(u8, 1000) == error.OutOfMemory);
}
test "create and destroy a single value" {
const allocator = std.testing.allocator;
// alloc/free is for slices; create/destroy is for one value.
const node = try allocator.create(Node);
defer allocator.destroy(node);
node.* = .{ .value = 42, .next = null };
try expect(node.value == 42);
}
const Node = struct {
value: u32,
next: ?*Node,
};
test "dupe copies a slice into owned memory" {
const allocator = std.testing.allocator;
// The classic use: keep a copy of a string whose original may go away.
const owned = try allocator.dupe(u8, "borrowed");
defer allocator.free(owned);
try expect(std.mem.eql(u8, owned, "borrowed"));
}
test "resize in place, or realloc to move" {
const allocator = std.testing.allocator;
var slice = try allocator.alloc(u8, 4);
// realloc keeps the contents and may return a new pointer.
slice = try allocator.realloc(slice, 8);
defer allocator.free(slice);
try expect(slice.len == 8);
}
test "DebugAllocator catches leaks in a real program" {
// In a program you own the allocator; this is what std.testing.allocator
// wraps for you. Formerly GeneralPurposeAllocator; renamed to say what it
// is for. `deinit` returns .leak if anything was not freed.
var debug: std.heap.DebugAllocator(.{}) = .init;
defer std.debug.assert(debug.deinit() == .ok);
const allocator = debug.allocator();
const data = try allocator.alloc(u8, 16);
allocator.free(data); // omit this and deinit reports .leak
}Choosing an allocator
The Allocator interface is one type, but the strategy behind it is yours to
pick:
| Allocator | Frees when | Good for |
|---|---|---|
ArenaAllocator | deinit(), all at once | Request handlers, compilers, anything phase-shaped |
FixedBufferAllocator | never (bump pointer) | Embedded, hot loops, no-heap contexts |
DebugAllocator | per-free, with leak detection | Development builds |
std.testing.allocator | per-free, fails the test on leak | Tests |
The arena is the one to reach for more often than you would think. If your
program has a natural “do a unit of work, then throw everything away” shape,
an arena turns hundreds of free calls into a single deinit, and removes a
whole category of leak.
defer is what makes this bearable
const numbers = try allocator.alloc(u32, 10);
defer allocator.free(numbers);
defer runs at scope exit, on every path, including early return and error
returns. Writing the free on the line directly after the alloc keeps the
pair visible in one glance, which is why Zig code reads the way it does.
Note the test above uses std.testing.allocator: if a test leaks, the test
fails. Leak checking is not an optional tool you remember to run, it is the
default in tests.
The operations you reach for
| Call | Gives you | Pair with |
|---|---|---|
alloc(T, n) | a []T of n items | free |
create(T) | a *T, one value | destroy |
dupe(T, slice) | an owned copy of slice | free |
realloc(slice, n) | the slice resized, contents kept | free |
create and destroy are the single-value pair, for a linked-list node or a
struct that outlives its stack frame; alloc/free are for slices. dupe is
how you keep a string whose original is about to disappear, the fix for the
dangling-key problem the Hash Maps chapter warns about. realloc may return a
new pointer, so always reassign: slice = try allocator.realloc(slice, n).
DebugAllocator was GeneralPurposeAllocator
If you arrived from an older tutorial looking for
std.heap.GeneralPurposeAllocator, it is now std.heap.DebugAllocator. The
rename states its job: a development allocator that tracks every allocation so
deinit() can return .leak when something was not freed. In a release build
you would swap it for std.heap.smp_allocator or the arena. Initialize it with
.init and read the leak result on the way out:
var debug: std.heap.DebugAllocator(.{}) = .init;
defer std.debug.assert(debug.deinit() == .ok);
const allocator = debug.allocator();