Memory Management and Allocators
18.1 Memory in Zig
Stack: locals, fixed-size arrays. Freed automatically at scope exit.
Static: globals, string literals, comptime data (in the binary).
Heap: only through an explicit std.mem.Allocator. There's no global malloc in Zig code unless you choose std.heap.c_allocator.
Functions that allocate take an allocator parameter. That makes memory behavior visible and testable.
18.2 The Allocator Interface
std.mem.Allocator is a fat pointer (ptr + vtable): a classic runtime interface.
fn example(allocator: std.mem.Allocator) !void {
const nums = try allocator.alloc(i32, 100); // slice of 100 (uninitialized)
defer allocator.free(nums);
const one = try allocator.create(Point); // a single item
defer allocator.destroy(one);
var grown = try allocator.alloc(u8, 10);
grown = try allocator.realloc(grown, 20); // may move
defer allocator.free(grown);
const copy = try allocator.dupe(u8, "text");
defer allocator.free(copy);
const zcopy = try allocator.dupeZ(u8, "c-string"); // null-terminated
defer allocator.free(zcopy);
const aligned = try allocator.alignedAlloc(u8, .@"64", 256); // 64-byte aligned (0.15 uses std.mem.Alignment)
defer allocator.free(aligned);
}18.3 Standard Allocators
std.heap.DebugAllocator(.{}) (formerly GeneralPurposeAllocator)
Development: detects leaks, double frees, use-after-free Fast, thread-safe, general-purpose (ReleaseFast production)
Directly maps OS pages (coarse, slow for small allocations) Allocate many items and free all at once (requests, parsers, CLI runs)
Allocate from a fixed stack/static buffer, with no syscalls libc malloc/free (requires linking libc) malloc without alignment wrappers Fast fixed-size object pool std.heap.stackFallback(N, fallback)
Stack buffer first, heap when it overflows Tests: fails the test on leaks Inject allocation failures in tests
18.4 Typical main Setup
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
var debug_alloc: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_alloc.deinit(); // reports leaks at exit
const allocator = if (builtin.mode == .Debug)
debug_alloc.allocator()
else
std.heap.smp_allocator;
try run(allocator);
}18.5 Arena Allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // frees everything at once
const a = arena.allocator();
const x = try a.alloc(u8, 100); // no individual free needed
const y = try a.create(Node);
_ = .{ x, y };
_ = arena.reset(.retain_capacity); // reuse memory between iterations18.6 Fixed Buffer Allocator
var buffer: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const alloc = fba.allocator();
const data = try alloc.alloc(u32, 10); // no heap, no syscalls (embedded-friendly)
_ = data;
fba.reset();18.7 Ownership Conventions
Document who frees returned memory. A common convention: functions that return allocated memory take the allocator, and the caller owns the result.
deinit(allocator) methods for container types (unmanaged style). toOwnedSlice() transfers ownership.
Pass allocator as the first parameter (after self).
18.8 Writing a Custom Allocator
The vtable signature changes between versions (remap was added in 0.14). Check std.mem.Allocator in your version.
const CountingAllocator = struct {
child: std.mem.Allocator,
count: usize = 0,
pub fn allocator(self: *CountingAllocator) std.mem.Allocator {
return .{ .ptr = self, .vtable = &.{
.alloc = alloc,
.resize = resize,
.remap = remap,
.free = free,
} };
}
fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ra: usize) ?[*]u8 {
const self: *CountingAllocator = @ptrCast(@alignCast(ctx));
self.count += 1;
return self.child.rawAlloc(len, alignment, ra);
}
fn resize(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) bool {
const self: *CountingAllocator = @ptrCast(@alignCast(ctx));
return self.child.rawResize(buf, alignment, new_len, ra);
}
fn remap(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) ?[*]u8 {
const self: *CountingAllocator = @ptrCast(@alignCast(ctx));
return self.child.rawRemap(buf, alignment, new_len, ra);
}
fn free(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, ra: usize) void {
const self: *CountingAllocator = @
…