Skip to content

Single Item Pointers

A pointer is a value that stores the address of another value.

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:

*T

Read *T as:

pointer to T

So:

*i32

means:

pointer to one i32 value

And:

*u8

means:

pointer to one u8 value

A 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:

*i32

because x is an i32.

The expression &x means:

the address of x

So after this code:

var x: i32 = 10;
const p = &x;

we can think of memory like this:

x: 10
p: address of x

The 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:

10

The expression:

p.*

means:

the value pointed to by p

This 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 = 25

This 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 i32

Read this as:

pointer to a constant i32

The 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
20

Here, 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:

QuestionControlled 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: *i32

That 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 = 11

Without 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 modify

This 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:

?*T

Example:

fn findNumber(found: bool, value: *i32) ?*i32 {
    if (found) {
        return value;
    } else {
        return null;
    }
}

The return type:

?*i32

means:

either a pointer to i32, or null

You 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.

*i32

A slice points to a sequence of values and stores a length.

[]i32

Example:

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

SyntaxMeaning
&xaddress of x
*Tpointer to mutable T
*const Tpointer to immutable T
p.*value pointed to by p
?*Toptional pointer to T
nullno 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 = 0

Notice the function signatures:

fn reset(counter: *Counter) void

This function modifies the counter.

fn printCounter(counter: *const Counter) void

This 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.