A pointer is a value that stores the address of another value.
In Zig, a single item pointer points to exactly one value. Its type looks like this:
*TRead *T as:
pointer to TSo:
*i32means:
pointer to one i32 valueAnd:
*u8means:
pointer to one u8 valueA pointer does not contain the value itself. It contains the location where the value lives.
Taking the Address of a Value
Use & to get the address of a value.
pub fn main() void {
var x: i32 = 10;
const p = &x;
_ = p;
}Here:
var x: i32 = 10;creates an integer value.
const p = &x;creates a pointer to x.
The type of p is:
*i32because x is an i32.
The expression &x means:
the address of xSo after this code:
var x: i32 = 10;
const p = &x;we can think of memory like this:
x: 10
p: address of xThe pointer p knows where x is stored.
Reading Through a Pointer
Use .* to access the value that a pointer points to.
const std = @import("std");
pub fn main() void {
var x: i32 = 10;
const p = &x;
std.debug.print("{}\n", .{p.*});
}Output:
10The expression:
p.*means:
the value pointed to by pThis is called dereferencing the pointer.
A pointer itself is an address. Dereferencing follows the address and gives you the value stored there.
Writing Through a Pointer
If the pointed-to value is mutable, you can change it through the pointer.
const std = @import("std");
pub fn main() void {
var x: i32 = 10;
const p = &x;
p.* = 25;
std.debug.print("x = {}\n", .{x});
}Output:
x = 25This line:
p.* = 25;does not change the pointer. It changes the value that the pointer points to.
The pointer still points to x.
The value of x changes from 10 to 25.
The Pointer Can Be Constant While the Value Is Mutable
This detail is important.
var x: i32 = 10;
const p = &x;Here, p is declared with const, but you can still write:
p.* = 20;Why?
Because const p means the pointer variable p cannot be changed to point somewhere else.
It does not mean the value behind the pointer is immutable.
Think of it like this:
p cannot point to a different value.
p may still modify x, because x is mutable.Example:
pub fn main() void {
var x: i32 = 10;
var y: i32 = 99;
const p = &x;
p.* = 20; // ok
// p = &y; // not ok: p itself is const
_ = y;
}The pointer binding is constant. The pointed-to value can still be changed.
Pointer to Const
Now compare that with this:
const x: i32 = 10;
const p = &x;Here, x is constant. So the pointer cannot be used to modify x.
pub fn main() void {
const x: i32 = 10;
const p = &x;
// p.* = 20; // error
}The type of p is not *i32.
It is:
*const i32Read this as:
pointer to a constant i32The pointer lets you read the value, but not modify it.
const std = @import("std");
pub fn main() void {
const x: i32 = 10;
const p = &x;
std.debug.print("{}\n", .{p.*});
}This is fine because reading is allowed.
Writing is not allowed.
Mutable Pointer Variable
Sometimes you want the pointer variable itself to change.
Then use var for the pointer binding:
const std = @import("std");
pub fn main() void {
var x: i32 = 10;
var y: i32 = 20;
var p = &x;
std.debug.print("{}\n", .{p.*});
p = &y;
std.debug.print("{}\n", .{p.*});
}Output:
10
20Here, p first points to x. Later, it points to y.
Because p is declared with var, the pointer itself can change.
So there are two separate questions:
| Question | Controlled by |
|---|---|
| Can the pointer variable point somewhere else? | const or var on the pointer binding |
| Can the pointed-to value be changed through the pointer? | whether the pointer is *T or *const T |
This distinction is central to Zig pointer code.
Passing a Pointer to a Function
Pointers are often used when a function needs to modify a value owned by the caller.
Example:
fn increment(x: *i32) void {
x.* += 1;
}
pub fn main() void {
var n: i32 = 10;
increment(&n);
}The function receives:
x: *i32That means x is a pointer to one mutable i32.
Inside the function:
x.* += 1;modifies the caller’s variable.
After the call, n becomes 11.
Full example:
const std = @import("std");
fn increment(x: *i32) void {
x.* += 1;
}
pub fn main() void {
var n: i32 = 10;
increment(&n);
std.debug.print("n = {}\n", .{n});
}Output:
n = 11Without a pointer, the function would receive a copy:
fn incrementWrong(x: i32) void {
var local = x;
local += 1;
}Changing local does not change the caller’s variable.
Use a pointer when the function needs access to the original value.
Passing a Pointer for Efficiency
Pointers are also useful when a value is large.
Suppose you have a large struct:
const Big = struct {
data: [4096]u8,
};Passing this by value may copy the whole struct.
fn process(value: Big) void {
_ = value;
}Passing a pointer avoids that copy:
fn process(value: *const Big) void {
_ = value;
}The type *const Big means:
pointer to a Big value that this function will not modifyThis is a common pattern:
fn readLargeThing(value: *const Big) void {
// inspect value, but do not modify it
}
fn modifyLargeThing(value: *Big) void {
// modify value
}Use *const T when the function only needs to read.
Use *T when the function needs to modify.
Returning Pointers
A function can return a pointer, but you must be careful.
This is wrong:
fn bad() *i32 {
var x: i32 = 10;
return &x;
}The variable x lives only while bad is running. When bad returns, x is gone. Returning &x would return a pointer to invalid memory.
That is a dangling pointer.
A better pattern is to return a pointer to memory that outlives the function call.
For example, the caller can provide the storage:
fn chooseFirst(a: *i32, b: *i32) *i32 {
_ = b;
return a;
}
pub fn main() void {
var x: i32 = 10;
var y: i32 = 20;
const p = chooseFirst(&x, &y);
p.* = 99;
}Here, x and y live in main. The returned pointer points to x, which is still valid after chooseFirst returns.
The key rule:
Do not return a pointer to a local variable that dies when the function returns.
Optional Pointers
Sometimes a pointer may or may not exist.
In Zig, this is represented with an optional pointer:
?*TExample:
fn findNumber(found: bool, value: *i32) ?*i32 {
if (found) {
return value;
} else {
return null;
}
}The return type:
?*i32means:
either a pointer to i32, or nullYou must check before using it:
const std = @import("std");
fn findNumber(found: bool, value: *i32) ?*i32 {
if (found) return value;
return null;
}
pub fn main() void {
var x: i32 = 42;
const result = findNumber(true, &x);
if (result) |p| {
std.debug.print("found: {}\n", .{p.*});
} else {
std.debug.print("not found\n", .{});
}
}Inside:
if (result) |p| {Zig unwraps the optional pointer. Inside that block, p is a normal *i32.
Single Item Pointer vs Slice
A single item pointer points to one value.
*i32A slice points to a sequence of values and stores a length.
[]i32Example:
var x: i32 = 10;
const p: *i32 = &x;p points to one integer.
Example:
var data = [_]i32{ 1, 2, 3 };
const s: []i32 = data[0..];s refers to several integers.
Do not confuse these two.
A single item pointer does not know how many items come after it. It is only for one value.
A slice knows its length, so it is usually better for arrays and buffers.
Pointer Syntax Summary
| Syntax | Meaning |
|---|---|
&x | address of x |
*T | pointer to mutable T |
*const T | pointer to immutable T |
p.* | value pointed to by p |
?*T | optional pointer to T |
null | no pointer value |
These forms appear constantly in Zig code.
A Complete Example
This program shows single item pointers in a practical way:
const std = @import("std");
const Counter = struct {
value: i32,
};
fn reset(counter: *Counter) void {
counter.value = 0;
}
fn printCounter(counter: *const Counter) void {
std.debug.print("counter = {}\n", .{counter.value});
}
fn add(counter: *Counter, amount: i32) void {
counter.value += amount;
}
pub fn main() void {
var counter = Counter{ .value = 10 };
printCounter(&counter);
add(&counter, 5);
printCounter(&counter);
reset(&counter);
printCounter(&counter);
}Output:
counter = 10
counter = 15
counter = 0Notice the function signatures:
fn reset(counter: *Counter) voidThis function modifies the counter.
fn printCounter(counter: *const Counter) voidThis function only reads the counter.
The pointer type documents the function’s intent.
The Main Idea
A single item pointer is a typed address to one value.
Use &x to get the address of x.
Use p.* to read or write the value behind the pointer.
Use *const T when the function should only read.
Use *T when the function may modify.
A pointer is powerful because it lets code work with the original value instead of a copy. That power comes with responsibility: the pointed-to memory must still be valid when the pointer is used.