Memory is where a program keeps its data while it runs.
Every variable, array, string, object, pointer, and function call uses memory in some way. When you write Zig, you are closer to memory than you are in languages like Python, JavaScript, Java, or Go. Zig does not hide memory from you. It makes memory part of the program’s design.
This is one of the most important ideas in Zig.
A Zig programmer should always be able to ask:
Where is this value stored?
How long does it live?
Who is allowed to read it?
Who is allowed to modify it?
Who is responsible for freeing it?
These questions are the beginning of Zig’s memory model.
Memory Is Just Bytes
At the lowest level, memory is a long sequence of bytes.
A byte is usually 8 bits. Each byte has an address. You can think of memory like a huge row of numbered boxes:
address: 1000 1001 1002 1003 1004 1005
value: 42 0 255 7 10 99A program does not usually work with raw bytes directly. It works with typed values.
For example:
const age: u8 = 42;This creates an unsigned 8-bit integer. It needs 1 byte.
const count: u32 = 1000;This creates an unsigned 32-bit integer. It needs 4 bytes.
The type tells Zig how many bytes the value needs and how those bytes should be interpreted.
A u8 and an i8 both use 1 byte, but they are interpreted differently. A u8 stores values from 0 to 255. An i8 stores values from -128 to 127.
Same bytes, different meaning.
Values Have Size
Every concrete type in Zig has a size.
You can ask Zig for the size of a type with @sizeOf:
const std = @import("std");
pub fn main() void {
std.debug.print("u8 size = {}\n", .{@sizeOf(u8)});
std.debug.print("u32 size = {}\n", .{@sizeOf(u32)});
std.debug.print("i64 size = {}\n", .{@sizeOf(i64)});
}Typical output:
u8 size = 1
u32 size = 4
i64 size = 8The unit is bytes.
This matters because memory use is not abstract in Zig. If you create an array of 1000 u64 values, that array needs 8000 bytes, because each u64 needs 8 bytes.
const numbers: [1000]u64 = undefined;The type [1000]u64 means “an array of 1000 unsigned 64-bit integers.”
Its size is:
1000 * 8 = 8000 bytesZig wants you to see this clearly.
Stack Memory
The stack is memory used for function calls and local variables.
When a function is called, Zig creates space for that function’s local data. When the function returns, that space is no longer valid.
Example:
fn addOne(x: i32) i32 {
const y = x + 1;
return y;
}Inside addOne, the parameter x and the local constant y live only while addOne is running.
After the function returns, they are gone.
This is normal stack memory behavior.
Stack memory is fast. It is also simple. You do not manually free stack memory. It is automatically reclaimed when the function exits.
Example:
pub fn main() void {
const x: i32 = 10;
const y: i32 = 20;
const z = x + y;
_ = z;
}Here, x, y, and z are local values. They are stored in memory associated with main.
For beginners, the important rule is:
Local variables usually live only until the end of the block or function that contains them.
Blocks Create Lifetimes
A block is code inside {}.
pub fn main() void {
const a = 10;
{
const b = 20;
_ = b;
}
_ = a;
}The variable a is visible until the end of main.
The variable b is visible only inside the inner block.
After the inner block ends, b cannot be used.
pub fn main() void {
{
const b = 20;
}
// This is invalid:
// _ = b;
}This is not only about names. It is also about lifetime. The storage for b belongs to the inner block.
Zig uses scopes to make lifetimes easier to reason about.
Heap Memory
The heap is memory used for data whose size or lifetime is not tied to one simple function call.
Stack memory is good when the compiler knows the size and lifetime clearly. Heap memory is useful when data must be created dynamically.
For example, suppose you do not know how many bytes you need until runtime. You can ask an allocator for memory:
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;
}This program allocates 1024 bytes on the heap.
The important lines are:
const buffer = try allocator.alloc(u8, 1024);This asks the allocator for a slice of 1024 u8 values.
defer allocator.free(buffer);This frees the memory when the current scope exits.
Zig does not have a garbage collector that automatically finds unused heap memory. If you allocate memory, you must free it, or design your allocator so cleanup happens in one known place.
Stack vs Heap
Stack and heap memory are both memory. The difference is how they are managed.
| Memory kind | Typical use | Lifetime | Freed by |
|---|---|---|---|
| Stack | Local values, function calls | Usually until function or block exits | Automatically |
| Heap | Dynamic data, large data, variable-sized data | Chosen by the program | Allocator/free logic |
Use the stack when the data is simple and local.
Use the heap when the data must be dynamically sized, returned, shared, or kept longer than one function call.
A fixed-size local array can live on the stack:
pub fn main() void {
var data: [4]u8 = .{ 1, 2, 3, 4 };
_ = data;
}A dynamically allocated array lives on the heap:
const data = try allocator.alloc(u8, count);
defer allocator.free(data);Here, count can be known only at runtime.
Pointers Are Addresses
A pointer stores the address of another value.
Example:
pub fn main() void {
var x: i32 = 10;
const p = &x;
p.* = 20;
}The expression &x means “the address of x.”
The variable p points to x.
The expression p.* means “the value that p points to.”
So this line:
p.* = 20;changes x to 20.
A pointer does not own memory by itself. It only refers to memory.
This distinction matters.
A value can exist. A pointer can point to it. But if the value stops existing, the pointer becomes invalid.
Dangling Pointers
A dangling pointer is a pointer that points to memory that is no longer valid.
Example:
fn badPointer() *i32 {
var x: i32 = 123;
return &x;
}This is wrong.
The variable x is local to badPointer. When badPointer returns, x is gone. Returning &x would give the caller a pointer to dead stack memory.
The memory may still contain the old bytes for a moment, but it no longer belongs to x. Using that pointer would be unsafe.
Zig is designed to catch many mistakes, but the programmer must still understand lifetimes. Low-level programming requires care.
Slices Are Pointer Plus Length
A slice is a view into a sequence of values.
A slice contains:
| Part | Meaning |
|---|---|
| pointer | where the first item is |
| length | how many items are in the slice |
Example:
pub fn main() void {
var data = [_]u8{ 10, 20, 30, 40 };
const slice = data[1..3];
_ = slice;
}The slice data[1..3] refers to the values:
20, 30It starts at index 1 and stops before index 3.
A slice does not copy the array. It views part of the same memory.
If you modify the slice, you modify the original array:
pub fn main() void {
var data = [_]u8{ 10, 20, 30, 40 };
const slice = data[1..3];
slice[0] = 99;
// data is now: 10, 99, 30, 40
}This is important. Slices are convenient, but they still point into existing memory. The original memory must remain valid while the slice is used.
Ownership
Ownership means responsibility for memory.
Zig does not have a built-in ownership system like Rust. Instead, Zig relies on explicit rules, clear APIs, and programmer discipline.
When you write Zig code, you should decide who owns each piece of memory.
Example:
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);Here, the current function owns buffer. It allocated the memory, so it must free the memory.
Now consider this function:
fn fill(buffer: []u8) void {
for (buffer) |*byte| {
byte.* = 0;
}
}This function receives a slice. It does not allocate the memory. It does not free the memory. It only uses the memory temporarily.
That is a common Zig pattern.
The caller owns the memory. The callee borrows it for a while.
Mutability
Zig separates mutable and immutable access.
const x = 10;x cannot be changed.
var y = 10;
y = 20;y can be changed.
This also affects pointers.
const x: i32 = 10;
const p = &x;Here, p points to a constant value. You cannot modify x through p.
var y: i32 = 10;
const p = &y;
p.* = 20;Here, y is mutable, so the pointer can be used to change it.
This helps make intent clear. If something should not change, use const.
Undefined Memory
Zig has a special value called undefined.
var x: i32 = undefined;This means: reserve memory for x, but do not initialize it with a meaningful value.
Reading x before assigning a real value is a bug.
var x: i32 = undefined;
// Wrong:
_ = x;But this is fine:
var x: i32 = undefined;
x = 42;
_ = x;undefined is useful when you want to initialize memory later. But beginners should use it carefully. Prefer real initial values unless you have a specific reason.
Memory Has Alignment
Types often need to be stored at addresses that match their alignment.
For example, a u32 is usually more efficient when stored at an address divisible by 4.
You can ask Zig for a type’s alignment:
const std = @import("std");
pub fn main() void {
std.debug.print("u8 align = {}\n", .{@alignOf(u8)});
std.debug.print("u32 align = {}\n", .{@alignOf(u32)});
std.debug.print("u64 align = {}\n", .{@alignOf(u64)});
}Alignment is a low-level detail, but it matters when working with pointers, packed structs, binary formats, C interop, and manual memory management.
For now, remember this:
A value has both a size and an alignment.
The Basic Memory Rules
For beginners, Zig memory can be understood with a small set of rules.
A value occupies bytes.
A type tells Zig how to interpret those bytes.
Local variables usually live on the stack.
Dynamically allocated memory usually lives on the heap.
Pointers store addresses.
Slices store a pointer and a length.
Allocated memory must be freed.
A pointer or slice must not outlive the memory it refers to.
const means the value cannot be changed through that binding.
undefined means memory exists, but its value is not meaningful yet.
These rules will appear again and again in Zig code.
A Complete Example
Here is a small program that uses stack memory, heap memory, a pointer, and a slice:
const std = @import("std");
pub fn main() !void {
var stack_number: i32 = 10;
const pointer = &stack_number;
pointer.* = 20;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const heap_buffer = try allocator.alloc(u8, 4);
defer allocator.free(heap_buffer);
heap_buffer[0] = 1;
heap_buffer[1] = 2;
heap_buffer[2] = 3;
heap_buffer[3] = 4;
const view = heap_buffer[1..3];
std.debug.print("stack_number = {}\n", .{stack_number});
std.debug.print("view = {any}\n", .{view});
}Output:
stack_number = 20
view = { 2, 3 }What happens here?
stack_number is a local variable. It lives on the stack.
pointer stores the address of stack_number.
pointer.* = 20 changes stack_number.
heap_buffer is allocated on the heap.
defer allocator.free(heap_buffer) ensures the heap memory is freed later.
view is a slice into heap_buffer.
The slice does not own the memory. It only views part of it.
This small example contains the main ideas of Zig memory: values, addresses, stack memory, heap memory, allocation, freeing, and borrowed views into memory.
The Main Idea
Zig does not try to make memory disappear.
Instead, Zig gives you a clear way to talk about memory directly.
This can feel hard at first, because you must think about details that other languages hide. But that is also the strength of Zig. You can write programs where memory use, lifetime, and ownership are visible in the code.
When memory is visible, bugs become easier to reason about.