Skip to content

Single-Item Pointers

A single-item pointer points to one value.

A single-item pointer points to one value.

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

The type *i32 means that p points to one i32.

The value is read with .*.

const y = p.*;

The value is written with .* when the pointed-to value is mutable.

p.* = 20;

A single-item pointer is not an array. It has no length. It does not say where the next item is. It says only that there is one value at this address.

const std = @import("std");

pub fn main() void {
    var n: i32 = 5;
    const p: *i32 = &n;

    p.* += 1;

    std.debug.print("{d}\n", .{n});
}

This prints:

6

A function often uses a single-item pointer when it must change one value supplied by the caller.

fn reset(n: *i32) void {
    n.* = 0;
}

The caller passes an address.

var count: i32 = 17;
reset(&count);

After the call, count is zero.

Single-item pointers are also useful for large structs. Passing a struct by value copies it. Passing a pointer copies only an address.

const Buffer = struct {
    data: [4096]u8,
    len: usize,
};

fn clear(buf: *Buffer) void {
    buf.len = 0;
}

Here buf points to one Buffer. The function changes the original buffer.

A single-item pointer may point to mutable data:

var x: i32 = 1;
const p: *i32 = &x;

It may also point to constant data:

const y: i32 = 1;
const q: *const i32 = &y;

A *const i32 can be read:

const z = q.*;

But it cannot be used to write:

q.* = 2; // error

This rule applies to struct fields too.

const Point = struct {
    x: i32,
    y: i32,
};

fn printPoint(p: *const Point) void {
    // p.x may be read
    // p.x may not be assigned
}

Use *const T when a function only observes a value.

fn lengthSquared(p: *const Point) i32 {
    return p.x * p.x + p.y * p.y;
}

Use *T when a function may change a value.

fn moveUp(p: *Point) void {
    p.y += 1;
}

This distinction belongs in the type. A caller can see from the function signature whether the function may mutate the value.

Single-item pointers do not support indexing.

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

// p[0] is not the ordinary way to use this pointer

Use p.* for the one item.

If you need to refer to many adjacent items, use a slice or a many-item pointer. Those are different types with different meaning.

The type says the promise:

*T        // one mutable T
*const T  // one constant T
[]T       // many mutable T values with a length
[]const T // many constant T values with a length

A single-item pointer may also be optional.

var x: i32 = 10;
var maybe: ?*i32 = &x;

The type ?*i32 means either a pointer to one i32, or null.

Before using it, unwrap it.

if (maybe) |p| {
    p.* += 1;
}

This form says: if maybe contains a pointer, call it p inside the block.

A non-optional pointer cannot be null. That is a major difference from C. If a Zig value has type *i32, it is expected to be a valid pointer to an i32. When a pointer may be absent, the type must say so with ?.

This small difference removes many accidental null checks from normal code. Null is not a hidden possibility. It is part of the type when it is possible.

Exercise 5-5. Write increment using a *u32.

Exercise 5-6. Write clearPoint that takes *Point and sets both fields to zero.

Exercise 5-7. Write printPoint that takes *const Point.

Exercise 5-8. Create a ?*i32, unwrap it with if, and change the pointed-to value.