The general purpose allocator is Zig’s standard allocator for ordinary heap allocation.
The general purpose allocator is Zig’s standard allocator for ordinary heap allocation.
Use it when your program needs heap memory, but you do not need a special strategy yet.
It can allocate memory of different sizes, free memory in any order, and help detect memory mistakes during development.
A small program usually starts like this:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
buffer[0] = 42;
}The important lines are these:
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();gpa is the allocator object. It owns the internal state needed to manage memory.
gpa.allocator() gives you a std.mem.Allocator value. That is the interface you pass to functions.
This distinction matters:
GeneralPurposeAllocator is the implementation.
std.mem.Allocator is the interface.Many Zig functions do not care which allocator implementation you use. They only need the allocator interface.
Why Use It
The general purpose allocator is useful because it behaves like the heap allocator many programmers expect.
You can allocate one buffer:
const bytes = try allocator.alloc(u8, 100);
defer allocator.free(bytes);You can allocate many buffers:
const a = try allocator.alloc(u8, 100);
defer allocator.free(a);
const b = try allocator.alloc(u8, 500);
defer allocator.free(b);
const c = try allocator.alloc(u8, 20);
defer allocator.free(c);You can free them in a different order from the order you allocated them.
That makes it flexible.
This flexibility costs more than a simple arena or fixed buffer allocator, but it is a good default while learning.
Detecting Leaks
One major reason to use the general purpose allocator during development is leak detection.
Look at this program:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
_ = try allocator.alloc(u8, 1024);
}This allocates memory but never frees it.
A careful program should do this instead:
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);When gpa.deinit() runs, the allocator can report that memory was leaked.
For beginner code, this is useful. It gives you feedback when you forget cleanup.
Checking deinit
You will often see this form:
defer {
const result = gpa.deinit();
if (result == .leak) {
std.debug.print("memory leak detected\n", .{});
}
}This checks whether the allocator found leaked memory.
A shorter version is common in examples:
defer _ = gpa.deinit();The short version ignores the result. That is acceptable for small examples, but in real debugging code, checking the result is better.
Passing the Allocator to Functions
A common pattern is to create the allocator once near the top of your program, then pass it into functions that need memory.
const std = @import("std");
fn makeMessage(allocator: std.mem.Allocator) ![]u8 {
return try std.fmt.allocPrint(allocator, "hello {s}", .{"zig"});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const message = try makeMessage(allocator);
defer allocator.free(message);
std.debug.print("{s}\n", .{message});
}The function makeMessage receives an allocator because it creates a new string.
The caller frees the returned string because the caller owns it.
This is the normal Zig ownership pattern:
The function allocates.
The function returns owned memory.
The caller frees it.Allocator Lifetime
The allocator must live longer than the memory it gives out.
This is correct:
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, 100);
defer allocator.free(buffer);The allocator is created first. The buffer is freed before gpa.deinit() runs.
This would be wrong in spirit:
const buffer = try allocator.alloc(u8, 100);
defer _ = gpa.deinit();
defer allocator.free(buffer);Because defer runs in reverse order, this would free the buffer after deinitializing the allocator. That is not the order you want.
Use this order:
defer _ = gpa.deinit();
const buffer = try allocator.alloc(u8, 100);
defer allocator.free(buffer);The later defer runs first, so the buffer is freed before the allocator is deinitialized.
Good Beginner Pattern
For early programs, use this structure:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const status = gpa.deinit();
if (status == .leak) {
std.debug.print("memory leak detected\n", .{});
}
}
const allocator = gpa.allocator();
// pass allocator to code that needs heap memory
const items = try allocator.alloc(i32, 10);
defer allocator.free(items);
items[0] = 123;
}This gives you a real heap allocator and a basic leak check.
When to Use Something Else
The general purpose allocator is a good default, but it is not always the best allocator.
Use an arena allocator when many allocations have the same lifetime and can be freed together.
Use a fixed buffer allocator when you want all allocation to come from a fixed block of memory.
Use the page allocator when you need direct page-level allocation or a simple backing allocator for another allocator.
The general purpose allocator is flexible. Specialized allocators are often simpler or faster for specific memory patterns.
The Core Idea
The general purpose allocator is the allocator you reach for when you need normal heap behavior.
It is useful while learning because it supports flexible allocation and helps expose leaks.
The main rule stays the same:
Every allocation needs a matching cleanup plan.