Skip to content

Optional Pointers

Pointers are often optional.

Pointers are often optional.

A search may not find an object. A tree node may not have a parent. A linked list node may not have a next node.

In Zig, this is written explicitly:

?*i32

This means:

either a pointer to i32, or null

A normal pointer:

*i32

cannot be null.

This difference matters.

Here is a simple example:

const std = @import("std");

pub fn main() void {
    var number: i32 = 123;

    const p: ?*i32 = &number;

    if (p) |ptr| {
        std.debug.print("{d}\n", .{ptr.*});
    }
}

The output is:

123

The expression:

ptr.*

dereferences the pointer.

The payload capture:

if (p) |ptr|

unwraps the optional pointer before use.

If the pointer is null:

const p: ?*i32 = null;

the block is skipped.

Optional pointers are common in data structures.

A linked list node might look like this:

const Node = struct {
    value: i32,
    next: ?*Node,
};

next may point to another node, or it may be null.

The last node in the list has:

next = null

This style is direct and compact. There is no hidden nullable state.

Here is a small traversal example:

const std = @import("std");

const Node = struct {
    value: i32,
    next: ?*Node,
};

pub fn main() void {
    var third = Node{
        .value = 30,
        .next = null,
    };

    var second = Node{
        .value = 20,
        .next = &third,
    };

    var first = Node{
        .value = 10,
        .next = &second,
    };

    var current: ?*Node = &first;

    while (current) |node| {
        std.debug.print("{d}\n", .{node.value});
        current = node.next;
    }
}

The output is:

10
20
30

Notice that current is optional:

var current: ?*Node = &first;

The loop ends when current becomes null.

Optional pointers are safer than nullable C pointers because the type system forces the possibility of null to be handled.

In C, this is legal:

int *p = NULL;
printf("%d\n", *p);

The program has undefined behavior.

In Zig, a plain pointer cannot hold null.

You must write:

?*i32

and unwrap it before dereferencing.

Optional pointers can also be combined with slices:

?[]u8

This means:

either a slice of bytes, or null

This is useful when a buffer may or may not exist.

Exercise 9-11. Write a function that returns a pointer to the largest element in an array.

Exercise 9-12. Modify the linked list example so it computes the sum of all node values.

Exercise 9-13. Why is ?*T safer than allowing *T to contain null silently?