Allocators & Memory
Allocators
An allocator in Zig is an interface over memory strategies. Passing one around changes nothing about what you allocate — it changes where the memory lives and who must eventually hand it back.
The Allocator Role
const std = @import("std");
// The signature says it all: this function needs memory.
fn buildGreeting(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
return std.fmt.allocPrint(allocator, "Hello, {s}!", .{name});
}
The std.heap Family
| Allocator | Behavior | Best for |
|---|---|---|
std.heap.page_allocator | Fresh pages from the OS each time | Prototypes, few allocations |
std.heap.c_allocator | Wraps malloc/free | Interop with C libraries |
std.heap.GeneralPurposeAllocator | Arena + safety + leak detection | Application heap (GPA) |
std.heap.ArenaAllocator | Allocations freed all at once | Request handlers, compilers |
std.heap.FixedBufferAllocator | Serves from a preallocated buffer | Embedded, zero-alloc paths |
std.testing.allocator | GPA that fails tests on leaks | Unit tests |
General-Purpose Allocator
The GPA is the production workhorse. Create it, grab its allocator, and let deinit verify that everything was freed.
Setup and Leak Checking
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); // returns true if leaks were found
const allocator = gpa.allocator();
const list = try allocator.alloc(u32, 8);
defer allocator.free(list); // without this, deinit reports a leak
ArenaAllocator
An arena hands out memory quickly and frees everything when it dies. It is the classic fit for per-request work: build a tree of objects, process it, drop the arena.
Batch Allocation
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // all arena memory returns at once
const alloc = arena.allocator();
const a = try alloc.alloc(u8, 64);
const b = try alloc.alloc(u8, 64);
// no individual frees — the arena owns everything
Figure 1 — the arena grows by allocation and releases everything at deinit; individual frees are unnecessary.
Allocating and Freeing
Four operations cover nearly all practice: alloc, free, realloc, and dupe. Each has a signed-free twin; the naming makes the free contract visible.
alloc and free
const buf = try allocator.alloc(u32, 4); // 4 uninitialized u32
buf[0] = 7;
std.debug.print("{d}\n", .{buf[0]});
// every alloc has a matching free — or a defer, or an arena
allocator.free(buf);
allocPrint — Building Strings
const title = try std.fmt.allocPrint(allocator, "Chapter {d}: {s}", .{ 3, "Memory" });
defer allocator.free(title);
std.ArrayList — Growable Memory
The dynamic array replaces hand-rolled realloc loops. It holds its own allocator, so the ownership contract is one line: deinit.
var names = std.ArrayList([]const u8).init(allocator);
defer names.deinit();
try names.append("Ada");
try names.append("Linus");
for (names.items) |name| std.debug.print("{s}\n", .{name});
Ownership Rules
No runtime borrow checker, but the convention is taught by every stdlib signature: the function that receives an allocator and returns allocated memory transfers ownership; the function that receives plain slices claims nothing.
Who Frees?
- You allocate, you free. Match every
allocwithfree,defer, or an arena. - Returned memory transfers. The caller owns it and must document the frees.
- Slices are views. Passing
[]const u8borrows; never free borrowed memory.
The errdefer Pattern
The failure path is where leaks hide. Allocate, errdefer free, do fallible work, then return success — one pattern, every layer.
fn loadName(allocator: std.mem.Allocator) ![]const u8 {
const name = try allocator.dupe(u8, "Sage");
errdefer allocator.free(name); // leak-free on every error path
const meta = try fetchMeta(); // may fail
return name; // success: caller owns name
}
Common Pitfalls
Leaking Memory
With the GPA, a leak is not silent: deinit reports it and tests fail. Free in a mirror order (LIFO) and use defer so early returns cannot skip cleanup.
Double Free
Freeing the same memory twice corrupts the allocator. Once ownership transfers, delete the old pointer — the GPA detects the double free, but only if you still have the pointer around.
Arena Lifetime Too Long
An arena held for a whole server run grows without bound. Bound arena lifetimes to requests or transactions, not to the process.
Next: Strings, Formatting & Files — text, output, and files through allocator eyes.