Memory is one of the most important ideas in Zig.
Many programming languages hide memory management from you. You create objects, strings, lists, maps, and buffers, and the language decides where the memory comes from and when it is released.
Zig takes a different approach. Zig wants memory use to be visible.
When a Zig function needs heap memory, it usually asks for an allocator.
An allocator is an object that gives memory to your program and later takes it back.
At a high level, an allocator answers two questions:
Where should this memory come from?
When and how should it be freed?Zig does not assume one global answer for the whole program. Instead, Zig lets each part of your program choose the right memory strategy.
Stack Memory and Heap Memory
Before understanding allocators, we need to separate two common kinds of memory: stack memory and heap memory.
Stack memory is used for local values inside functions.
pub fn main() void {
const x: i32 = 123;
const y: bool = true;
_ = x;
_ = y;
}Here, x and y are simple local values. They live inside main. When main ends, they are gone.
Stack memory is fast and simple, but it has limits. It works well when the compiler knows the size of the value ahead of time.
For example:
const numbers = [_]u8{ 1, 2, 3, 4 };This array has exactly 4 items. Its size is known at compile time.
But sometimes the size is not known until the program runs.
For example, imagine reading a file:
How large is the file?
How many bytes do we need?The answer depends on the actual file. The compiler cannot know it in advance.
That is when heap memory becomes useful.
Heap memory is memory requested while the program is running. You can ask for 10 bytes, 1,000 bytes, or 1,000,000 bytes depending on runtime data.
In Zig, heap memory usually comes from an allocator.
The Basic Allocator Idea
A Zig allocator is usually passed into a function like this:
const std = @import("std");
fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
const buffer = try allocator.alloc(u8, 1024);
return buffer;
}This function asks the allocator for 1024 bytes.
The return type is:
![]u8This means the function returns either:
an erroror:
a slice of bytesAllocation can fail. The computer may not have enough memory. The allocator may have a fixed limit. The operating system may refuse the request.
That is why this line uses try:
const buffer = try allocator.alloc(u8, 1024);The word try means: if allocation fails, return the error from the current function.
Zig makes allocation failure visible.
Allocated Memory Must Be Freed
When you allocate memory, you must eventually free it.
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
buffer[0] = 42;
}This line allocates memory:
const buffer = try allocator.alloc(u8, 1024);This line frees it later:
defer allocator.free(buffer);The defer keyword means: run this statement when the current scope exits.
So the program does this:
allocate buffer
use buffer
free buffer when main exitsThis is a common Zig pattern.
Why Not Use a Garbage Collector?
A garbage collector automatically finds unused memory and frees it later.
That can be convenient, but it has tradeoffs.
A garbage collector usually means the runtime has extra work to do. It may pause the program. It may use more memory. It may make performance harder to predict.
Zig is often used for software where predictable behavior matters:
operating systems
games
databases
network servers
embedded devices
command-line tools
compilersIn these programs, memory behavior is part of the design.
Zig does not forbid garbage collection. You could build or use one if you wanted. But Zig does not make garbage collection the default model.
Instead, Zig gives you allocators.
Why Not Use One Global Allocator?
Some languages have one common heap for nearly everything.
Zig avoids making that the only model.
Different parts of a program may need different allocation behavior.
A short-lived parser may want an arena allocator. It can allocate many small objects and free them all at once.
A server may want a general-purpose allocator that can allocate and free memory throughout the lifetime of the process.
An embedded program may want a fixed buffer allocator because dynamic memory from the operating system is not available.
A test may want an allocator that detects leaks.
By passing an allocator explicitly, Zig lets you choose.
Allocators Make APIs Honest
Consider this function name:
fn readWholeFile(...) ...Does it allocate memory?
In some languages, you may need to check the documentation or source code to know.
In Zig, the function usually tells you through its parameters:
fn readWholeFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
// ...
}The allocator parameter is a signal.
It tells the caller:
This function needs memory.
You decide where that memory comes from.
You are responsible for freeing the returned memory.This is one of the most important habits in Zig programming.
Memory allocation should not be hidden when it matters.
Allocators Help Testing
Allocators are also useful in tests.
Zig has a testing allocator:
const std = @import("std");
test "allocate a buffer" {
const allocator = std.testing.allocator;
const buffer = try allocator.alloc(u8, 10);
defer allocator.free(buffer);
try std.testing.expect(buffer.len == 10);
}The testing allocator can help detect memory leaks during tests.
If you allocate memory and forget to free it, the test system can report the problem.
This is a major benefit. You can catch memory mistakes early, before they become production bugs.
Allocators Help Performance
Allocation is not free.
Every heap allocation has some cost. If a program allocates memory too often, it may become slower.
Zig makes allocation easier to notice because allocators are explicit.
When you see this:
try allocator.alloc(u8, size);you know memory is being requested.
That visibility helps you improve performance.
You can ask:
Can this use stack memory instead?
Can this reuse an existing buffer?
Can this use an arena allocator?
Can this allocate once instead of many times?
Can the caller provide the memory?Zig code often becomes faster because memory decisions are made deliberately.
Allocators Help Structure Programs
Passing allocators also helps organize ownership.
For example:
const User = struct {
name: []u8,
pub fn deinit(self: User, allocator: std.mem.Allocator) void {
allocator.free(self.name);
}
};This struct owns the memory for name.
Because it owns that memory, it provides a deinit function to release it.
A common Zig pattern is:
init allocates resources
deinit releases resourcesExample:
const std = @import("std");
const User = struct {
name: []u8,
pub fn init(allocator: std.mem.Allocator, name: []const u8) !User {
const copy = try allocator.dupe(u8, name);
return User{ .name = copy };
}
pub fn deinit(self: User, allocator: std.mem.Allocator) void {
allocator.free(self.name);
}
};
pub fn main() !void {
const allocator = std.heap.page_allocator;
const user = try User.init(allocator, "Ada");
defer user.deinit(allocator);
}Read this as a lifecycle:
User.init creates a User and allocates memory for the name.
User.deinit frees the memory owned by the User.This style is explicit and predictable.
Common Allocators in Zig
You will meet several allocator types in Zig.
page_allocator asks the operating system for memory pages. It is simple, but often too low-level for frequent small allocations.
GeneralPurposeAllocator is a general allocator useful for many programs. It can also help detect memory problems.
ArenaAllocator is useful when you want to allocate many things and free them all at once.
FixedBufferAllocator uses a fixed slice of memory as its backing storage. It is useful when you want strict memory limits.
You do not need to master all of them immediately. At the beginning, focus on the idea:
An allocator is how Zig code asks for heap memory.The Important Rule
In Zig, allocation and ownership should be clear.
When you write a function that allocates memory, ask yourself:
Who provides the allocator?
Who owns the returned memory?
Who frees it?
When is it freed?
What happens if allocation fails?These questions are not optional details. They are part of the design of the function.
A good Zig API makes the answers visible.
Summary
Zig uses allocators because memory matters.
Allocators make memory allocation explicit. They make errors visible. They let different parts of a program use different memory strategies. They help testing, performance, and ownership.
The beginner rule is simple:
If your code asks an allocator for memory, your code must also have a clear plan to free that memory.That rule will guide most of your early Zig programs.