Allocators & Memory

Zig performs no hidden allocation: any memory your program uses above the stack comes from an allocator you chose and passed in. This lesson turns that slogan into reflexes — which allocator for which job, who frees what, and how the compiler helps you find leaks.

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

AllocatorBehaviorBest for
std.heap.page_allocatorFresh pages from the OS each timePrototypes, few allocations
std.heap.c_allocatorWraps malloc/freeInterop with C libraries
std.heap.GeneralPurposeAllocatorArena + safety + leak detectionApplication heap (GPA)
std.heap.ArenaAllocatorAllocations freed all at onceRequest handlers, compilers
std.heap.FixedBufferAllocatorServes from a preallocated bufferEmbedded, zero-alloc paths
std.testing.allocatorGPA that fails tests on leaksUnit tests
Thinking in Zig: in other languages the heap is an invisible god that never says no. In Zig you choose the policy per function call and the policy is enforced: page_allocator will fail on exotic requests, FixedBufferAllocator will fail when its buffer runs out, and the GPA will tell you about every byte you forgot to free.

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
arena allocation lifetime

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 alloc with free, defer, or an arena.
  • Returned memory transfers. The caller owns it and must document the frees.
  • Slices are views. Passing []const u8 borrows; 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.