Skip to content

Pointer Arithmetic

Pointer arithmetic means forming a new pointer by moving from one element to another.

Pointer arithmetic means forming a new pointer by moving from one element to another.

In Zig, ordinary single-item pointers do not support pointer arithmetic.

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

// p + 1 is not the right operation here

The type *i32 means one i32. There is no second item promised by the type.

Many-item pointers do support pointer arithmetic.

var items = [_]i32{ 10, 20, 30 };
const p: [*]i32 = &items;

const q = p + 1;

The pointer q points one element after p.

q[0] // same as p[1]

Here is a complete program.

const std = @import("std");

pub fn main() void {
    var items = [_]i32{ 10, 20, 30 };

    const p: [*]i32 = &items;
    const q = p + 1;

    std.debug.print("{d}\n", .{q[0]});
}

It prints:

20

The addition is measured in elements, not bytes. Since p points to i32, p + 1 moves by one i32.

Subtraction works in the same way.

const r = q - 1;

Now r points to the same item as p.

Pointer arithmetic does not carry bounds. If p points to the first element of a three-element array, then p + 3 points just past the last element. It must not be read as an item.

const end = p + 3;

// end[0] is outside the array

A pointer past the end may be useful as a marker. It is not a pointer to a valid element.

Because many-item pointers do not store a length, the program must know the bounds by some other means.

fn printMany(ptr: [*]const i32, len: usize) void {
    var i: usize = 0;

    while (i < len) : (i += 1) {
        std.debug.print("{d}\n", .{ptr[i]});
    }
}

The pointer alone is not enough. The length is a separate argument.

Slices make this safer.

fn printSlice(items: []const i32) void {
    for (items) |item| {
        std.debug.print("{d}\n", .{item});
    }
}

A slice contains both pointer and length. Most ordinary Zig code should use a slice instead of doing pointer arithmetic.

Pointer arithmetic is mainly useful near low-level interfaces: C APIs, manual memory handling, device buffers, and algorithms that are written directly over raw memory.

A byte pointer is often used when the program wants to move by bytes.

var bytes = [_]u8{ 1, 2, 3, 4 };
const p: [*]u8 = &bytes;

const second = p + 1;

Here second points to the second byte.

For a larger type, pointer movement is still by elements.

var nums = [_]u32{ 1, 2, 3 };
const p: [*]u32 = &nums;

const next = p + 1;

next points to the second u32, not the next byte.

Indexing is often clearer than addition.

p[2]

This means the same item as:

(p + 2)[0]

Prefer the first form unless the pointer itself must move.

A common pattern is to advance a pointer while a count decreases.

fn sumMany(ptr: [*]const i32, len: usize) i32 {
    var p = ptr;
    var n = len;
    var total: i32 = 0;

    while (n != 0) : ({
        p += 1;
        n -= 1;
    }) {
        total += p[0];
    }

    return total;
}

This works, but the slice version is usually better.

fn sumSlice(items: []const i32) i32 {
    var total: i32 = 0;

    for (items) |item| {
        total += item;
    }

    return total;
}

The slice version says less about addresses and more about the data.

Pointer arithmetic can also be used with sentinel pointers.

fn lenZ(ptr: [*:0]const u8) usize {
    var p = ptr;
    var n: usize = 0;

    while (p[0] != 0) : ({
        p += 1;
        n += 1;
    }) {}

    return n;
}

The sentinel value 0 marks the end. The pointer moves one byte at a time until that value is found.

This is the shape of many C string operations. Zig can express it, but ordinary Zig strings are usually slices:

[]const u8

A slice already has a length, so no sentinel scan is needed.

Pointer arithmetic is a low-level operation. It is useful when the type and the surrounding code provide enough information to prove that the movement is valid.

The rule is simple: move only inside storage that really contains adjacent elements, and never read outside the valid range.

Exercise 5-17. Create an array of four integers and use a many-item pointer to print the second and third elements.

Exercise 5-18. Write sumMany using pointer arithmetic and a length.

Exercise 5-19. Rewrite sumMany using a slice.

Exercise 5-20. Write a function that counts bytes in a [*:0]const u8 until it reaches the zero sentinel.