Skip to content

Many-Item Pointers

A many-item pointer points to the first item in a sequence.

A many-item pointer points to the first item in a sequence.

Its type is written with [*].

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

The type [*]i32 means “pointer to many i32 values.” It does not store a length. It says where the first item is, but not how many items may be used.

Items are accessed with an index.

const first = p[0];
const second = p[1];

This program prints the first three values.

const std = @import("std");

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

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

The output is:

10 20 30

A many-item pointer is close to a C pointer. It may be indexed. It may be moved with pointer arithmetic. But because it has no length, it must be used carefully.

const q = p + 1;
std.debug.print("{d}\n", .{q[0]});

Here q points at the second element of the same array. q[0] is the same value as p[1].

Many-item pointers are useful when working with C APIs, raw memory, or sentinel-terminated data. They are less common in ordinary Zig code. Most Zig code uses slices instead.

A slice has a pointer and a length.

var items = [_]i32{ 10, 20, 30 };
const s: []i32 = items[0..];

The slice s knows that it contains three elements.

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

A many-item pointer does not know this.

const p: [*]i32 = &items;
// p has no len field

This is the main difference.

Use a many-item pointer when the length is known by some other rule. Use a slice when the length should travel with the pointer.

A function that takes a many-item pointer must get the length separately if it needs bounds.

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

    while (i < len) : (i += 1) {
        total += ptr[i];
    }

    return total;
}

The caller must pass both values.

const result = sum(&items, items.len);

This is a common C shape: pointer plus length.

In Zig, the same function is usually clearer with a slice.

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

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

    return total;
}

The slice version carries the length in the argument itself.

Many-item pointers may point to mutable or constant data.

const p: [*]i32 = &items;        // may write items
const q: [*]const i32 = &items;  // may only read items

Writing through a mutable many-item pointer changes the original storage.

p[0] = 99;

Reading through a constant many-item pointer is allowed.

const x = q[0];

Writing through it is not.

q[0] = 99; // error

Many-item pointers can also be sentinel-terminated.

const name: [*:0]const u8 = "zig";

The type [*:0]const u8 means a many-item pointer to bytes, ending at a zero byte. This is the shape used by many C strings.

The sentinel is not a length. It is a value that marks the end. Code must scan until it finds the sentinel.

For normal Zig strings, use slices.

const name: []const u8 = "zig";

For C strings, sentinel pointers are often needed.

const c_name: [*:0]const u8 = "zig";

A many-item pointer is a low-level tool. It is powerful because it contains little information. It is dangerous for the same reason.

The rule is simple: if the number of elements matters, prefer a slice. Use a many-item pointer only when the size is known elsewhere, or when an external interface requires it.

Exercise 5-9. Write a function first that takes [*]const i32 and returns the first item.

Exercise 5-10. Write a function sumMany that takes [*]const i32 and a length.

Exercise 5-11. Rewrite sumMany to take []const i32.

Exercise 5-12. Create a many-item pointer to an array and modify the second element through it.