Zig Memory Management
Memory management is a core concept in systems programming. Zig provides powerful and flexible memory management mechanisms that allow developers to precisely control memory allocation and deallocation.
Memory Management Basics
Stack Memory vs Heap Memory
zig
const std = @import("std");
pub fn main() void {
// Stack memory: automatically managed, freed when function ends
var stack_array: [100]i32 = undefined;
const stack_value = 42;
std.debug.print("Stack variable address: {*}\n", .{&stack_value});
std.debug.print("Stack array address: {*}\n", .{&stack_array});
// Stack memory lifetime is determined by scope
{
var local_var = 123;
std.debug.print("Local variable address: {*}\n", .{&local_var});
} // local_var is destroyed here
}Allocator Concept
Zig uses allocators to manage heap memory:
zig
const std = @import("std");
pub fn main() !void {
// Create general purpose allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Allocate memory
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory); // Important: free memory
std.debug.print("Allocated {} bytes of memory\n", .{memory.len});
std.debug.print("Memory address: {*}\n", .{memory.ptr});
// Use memory
for (memory, 0..) |*byte, i| {
byte.* = @intCast(i % 256);
}
std.debug.print("First 10 bytes: ");
for (memory[0..10]) |byte| {
std.debug.print("{} ", .{byte});
}
std.debug.print("\n");
}Allocator Types
General Purpose Allocator
zig
const std = @import("std");
pub fn main() !void {
// General purpose allocator: suitable for most uses, has memory leak detection
var gpa = std.heap.GeneralPurposeAllocator(.{
.safety = true, // Enable safety checks
.thread_safe = true, // Thread safe
}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("Memory leak detected!\n", .{});
}
}
const allocator = gpa.allocator();
// Allocate different sizes of memory
const small_mem = try allocator.alloc(u8, 10);
const large_mem = try allocator.alloc(i32, 1000);
defer allocator.free(small_mem);
defer allocator.free(large_mem);
std.debug.print("Small memory: {} bytes\n", .{small_mem.len});
std.debug.print("Large memory: {} integers\n", .{large_mem.len});
}Page Allocator
zig
const std = @import("std");
pub fn main() !void {
// Page allocator: allocates pages directly from OS
const allocator = std.heap.page_allocator;
// Allocate large blocks of memory
const big_memory = try allocator.alloc(u8, 4096 * 10); // 10 pages
defer allocator.free(big_memory);
std.debug.print("Allocated {} bytes ({} pages)\n", .{ big_memory.len, big_memory.len / 4096 });
// Page allocator is suitable for large memory allocations
const huge_array = try allocator.alloc(f64, 100000);
defer allocator.free(huge_array);
std.debug.print("Allocated {} floats\n", .{huge_array.len});
}Fixed Buffer Allocator
zig
const std = @import("std");
pub fn main() !void {
// Pre-allocated buffer
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// Allocate from fixed buffer
const mem1 = try allocator.alloc(u8, 100);
const mem2 = try allocator.alloc(i32, 50);
std.debug.print("Allocated from fixed buffer:\n");
std.debug.print(" Memory 1: {} bytes\n", .{mem1.len});
std.debug.print(" Memory 2: {} integers\n", .{mem2.len});
std.debug.print(" Remaining space: {} bytes\n", .{fba.end_index});
// Fixed buffer allocator doesn't need explicit freeing
// All memory is freed when fba goes out of scope
}Arena Allocator
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// Create arena allocator
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit(); // Free all memory at once
const allocator = arena.allocator();
// Allocate multiple memory blocks
var allocations = std.ArrayList([]u8).init(gpa.allocator());
defer allocations.deinit();
for (0..10) |i| {
const size = (i + 1) * 10;
const memory = try allocator.alloc(u8, size);
try allocations.append(memory);
std.debug.print("Allocation {}: {} bytes\n", .{ i, size });
}
std.debug.print("Total allocations: {}\n", .{allocations.items.len});
// arena.deinit() frees all allocated memory
// No need to free each memory block individually
}Memory Allocation and Deallocation
Basic Allocation Operations
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// alloc: allocate uninitialized memory
const uninit_memory = try allocator.alloc(i32, 5);
defer allocator.free(uninit_memory);
// Initialize memory
for (uninit_memory, 0..) |*item, i| {
item.* = @intCast(i * 10);
}
std.debug.print("Uninitialized memory: ");
for (uninit_memory) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// allocSentinel: allocate memory with sentinel value
const sentinel_memory = try allocator.allocSentinel(u8, 10, 0);
defer allocator.free(sentinel_memory);
// Copy string to sentinel memory
@memcpy(sentinel_memory[0..5], "Hello");
std.debug.print("Sentinel memory: {s}\n", .{sentinel_memory});
// dupe: duplicate existing data
const original = [_]i32{ 1, 2, 3, 4, 5 };
const duplicated = try allocator.dupe(i32, &original);
defer allocator.free(duplicated);
std.debug.print("Duplicated data: ");
for (duplicated) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
}Memory Safety
Memory Leak Detection
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.safety = true,
}){};
const allocator = gpa.allocator();
// Normal memory usage
const good_memory = try allocator.alloc(u8, 100);
allocator.free(good_memory);
// Intentionally create memory leak (for demonstration only)
const leaked_memory = try allocator.alloc(u8, 50);
_ = leaked_memory; // Intentionally not freed
// Check for memory leaks
const leaked = gpa.deinit();
switch (leaked) {
.ok => std.debug.print("No memory leaks\n", .{}),
.leak => std.debug.print("Memory leak detected!\n", .{}),
}
}Memory Management Best Practices
1. RAII Pattern
zig
const std = @import("std");
const ManagedBuffer = struct {
data: []u8,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(allocator: std.mem.Allocator, size: usize) !Self {
const data = try allocator.alloc(u8, size);
return Self{
.data = data,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.data);
}
pub fn fill(self: *Self, value: u8) void {
@memset(self.data, value);
}
pub fn getData(self: *const Self) []const u8 {
return self.data;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Use RAII pattern to manage memory
var buffer = try ManagedBuffer.init(allocator, 100);
defer buffer.deinit(); // Automatic cleanup
buffer.fill(42);
std.debug.print("Buffer size: {}\n", .{buffer.getData().len});
std.debug.print("First 5 bytes: ");
for (buffer.getData()[0..5]) |byte| {
std.debug.print("{} ", .{byte});
}
std.debug.print("\n");
}2. Use defer to Ensure Cleanup
zig
const std = @import("std");
fn processData(allocator: std.mem.Allocator) !void {
// Allocate multiple resources
const buffer1 = try allocator.alloc(u8, 1000);
defer allocator.free(buffer1);
const buffer2 = try allocator.alloc(i32, 500);
defer allocator.free(buffer2);
const buffer3 = try allocator.alloc(f64, 200);
defer allocator.free(buffer3);
// Process data...
std.debug.print("Processing {} + {} + {} bytes of data\n",
.{ buffer1.len, buffer2.len * @sizeOf(i32), buffer3.len * @sizeOf(f64) });
// All defer statements execute automatically at function end
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try processData(allocator);
std.debug.print("Data processing complete, memory cleaned up\n", .{});
}Summary
This chapter covered Zig's memory management system in detail:
- ✅ Difference between stack and heap memory
- ✅ Various allocator types and their use cases
- ✅ Memory allocation, deallocation, and resizing
- ✅ Memory alignment and safety checks
- ✅ Custom allocator implementation
- ✅ Memory pool and object pool patterns
- ✅ Memory management best practices
Zig's memory management system provides great flexibility and control, allowing developers to choose the most appropriate memory management strategy based on specific needs. In the next chapter, we'll learn about Zig's compile-time features.