Manual Memory Management
Zig provides manual memory management without a garbage collector. You explicitly allocate and free memory using allocators.
Zig’s approach to memory management gives you full control while providing safety checks in Debug and ReleaseSafe modes.
Allocators
Allocators in Zig are represented by the std.mem.Allocator interface:
const std = @import ( "std" );
const Allocator = std . mem . Allocator ;
const expect = std . testing . expect ;
test "using an allocator" {
var buffer : [ 100 ] u8 = undefined ;
var fba = std . heap . FixedBufferAllocator . init ( & buffer );
const allocator = fba . allocator ();
const result = try concat ( allocator , "foo" , "bar" );
try expect ( std . mem . eql ( u8 , "foobar" , result ));
}
fn concat ( allocator : Allocator , a : [] const u8 , b : [] const u8 ) ! [] u8 {
const result = try allocator . alloc ( u8 , a . len + b . len );
@memcpy ( result [ 0 .. a . len ], a );
@memcpy ( result [ a . len ..], b );
return result ;
}
Passing allocators as parameters makes memory allocation explicit and testable.
Common Allocators
Page Allocator
Direct system memory allocator:
const allocator = std . heap . page_allocator ;
const memory = try allocator . alloc ( u8 , 1024 );
defer allocator . free ( memory );
Arena Allocator
Allocates memory that’s freed all at once:
const std = @import ( "std" );
pub fn main () ! void {
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit ();
const allocator = arena . allocator ();
const ptr = try allocator . create ( i32 );
std . debug . print ( "ptr={} \n " , .{ ptr });
}
Arena allocators are perfect for request-scoped allocations where you want to free everything at once.
Fixed Buffer Allocator
Allocates from a fixed-size buffer:
var buffer : [ 1024 ] u8 = undefined ;
var fba = std . heap . FixedBufferAllocator . init ( & buffer );
const allocator = fba . allocator ();
General Purpose Allocator
Detects memory leaks and use-after-free:
var gpa = std . heap . GeneralPurposeAllocator (.{}){};
defer _ = gpa . deinit ();
const allocator = gpa . allocator ();
Page Allocator
Arena Allocator
GPA
Fixed Buffer
const allocator = std . heap . page_allocator ;
// Simple, direct system allocator
// No tracking or safety checks
var arena = std . heap . ArenaAllocator . init ( backing_allocator );
defer arena . deinit ();
const allocator = arena . allocator ();
// Batch free all allocations
var gpa = std . heap . GeneralPurposeAllocator (.{}){};
defer _ = gpa . deinit ();
const allocator = gpa . allocator ();
// Detects leaks and use-after-free
var buffer : [ 1024 ] u8 = undefined ;
var fba = std . heap . FixedBufferAllocator . init ( & buffer );
const allocator = fba . allocator ();
// Stack-based allocation
Allocator Interface
Allocating Memory
// Allocate a slice
const slice = try allocator . alloc ( u8 , 100 );
defer allocator . free ( slice );
// Allocate a single item
const item = try allocator . create ( MyStruct );
defer allocator . destroy ( item );
// Reallocate (resize)
const new_slice = try allocator . realloc ( slice , 200 );
Freeing Memory
// Free a slice
allocator . free ( slice );
// Destroy a single item
allocator . destroy ( item );
Always pair allocations with frees. Use defer to ensure cleanup happens even on error paths.
Memory Management Patterns
RAII with Defer
fn processData ( allocator : Allocator ) ! void {
const data = try allocator . alloc ( u8 , 1024 );
defer allocator . free ( data );
// Use data...
// Automatically freed when function returns
}
Errdefer for Cleanup
fn createResource ( allocator : Allocator ) !* Resource {
const resource = try allocator . create ( Resource );
errdefer allocator . destroy ( resource );
try resource . init ();
errdefer resource . deinit ();
try resource . configure ();
return resource ;
}
Use errdefer to clean up resources only when an error occurs.
ArrayList
Dynamic arrays with automatic resizing:
const std = @import ( "std" );
test "ArrayList" {
var list = std . ArrayList ( i32 ). init ( std . testing . allocator );
defer list . deinit ();
try list . append ( 1 );
try list . append ( 2 );
try list . append ( 3 );
try expect ( list . items . len == 3 );
try expect ( list . items [ 0 ] == 1 );
}
HashMap
Hash maps for key-value storage:
test "HashMap" {
var map = std . StringHashMap ( i32 ). init ( std . testing . allocator );
defer map . deinit ();
try map . put ( "foo" , 42 );
try map . put ( "bar" , 100 );
try expect ( map . get ( "foo" ). ? == 42 );
}
Stack vs Heap
Stack Allocation
Heap Allocation
fn stackExample () void {
var array : [ 100 ] u8 = undefined ;
// Automatically freed when function returns
}
Memory Alignment
Control memory alignment for performance:
test "aligned allocation" {
const allocator = std . testing . allocator ;
const aligned_ptr = try allocator . alignedAlloc ( u8 , 16 , 64 );
defer allocator . free ( aligned_ptr );
try expect ( @intFromPtr ( aligned_ptr . ptr ) % 16 == 0 );
}
Memory Safety
Zig provides several safety features:
Undefined Behavior Detection
Use-after-free detection (with GPA)
Double-free detection
Memory leak detection
Bounds checking on slices
Build Modes
Debug
ReleaseSafe
ReleaseFast
ReleaseSmall
Full safety checks, slow execution, large binary Most safety checks, optimized, larger binary zig build -Doptimize=ReleaseSafe
Minimal safety, fastest execution, smaller binary zig build -Doptimize=ReleaseFast
Minimal safety, optimized for size zig build -Doptimize=ReleaseSmall
Ownership Patterns
Zig doesn’t enforce ownership rules at compile-time, but conventions exist:
const Resource = struct {
data : [] u8 ,
allocator : Allocator ,
pub fn init ( allocator : Allocator , size : usize ) ! Resource {
return Resource {
. data = try allocator . alloc ( u8 , size ),
. allocator = allocator ,
};
}
pub fn deinit ( self : * Resource ) void {
self . allocator . free ( self . data );
}
};
test "resource management" {
var resource = try Resource . init ( std . testing . allocator , 100 );
defer resource . deinit ();
// Use resource...
}
Store the allocator in structs that manage their own memory for clean deinitialization.
Testing Allocator
Use the testing allocator to detect leaks in tests:
test "no leaks" {
const allocator = std . testing . allocator ;
const data = try allocator . alloc ( u8 , 100 );
defer allocator . free ( data );
// Test will fail if we forget to free
}
Best Practices
Always use defer to pair allocations with frees
Use errdefer for cleanup on error paths
Pass allocators as function parameters
Use Arena allocators for temporary allocations
Use the testing allocator in tests to detect leaks
Store allocators in structs that manage memory
Prefer stack allocation when size is known and small
Use the General Purpose Allocator during development
Memory leaks won’t be detected at compile-time. Use the testing allocator and General Purpose Allocator to find them during development.