⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Many-item Pointers

const std = @import("std");
const expect = std.testing.expect;

test "many-item pointers support indexing" {
    var array = [_]u8{ 1, 2, 3, 4 };
    const many: [*]u8 = &array;

    // Indexing and pointer arithmetic are allowed, but there is no `.len`
    // and therefore no bounds checking. You are responsible for the range.
    try expect(many[0] == 1);
    try expect(many[3] == 4);
}

test "convert to a slice by supplying the length" {
    var array = [_]u8{ 1, 2, 3, 4 };
    const many: [*]u8 = &array;

    // The length is the piece of information `[*]T` is missing; adding it
    // back yields a `[]T`, which is bounds-checked again.
    const slice: []u8 = many[0..3];
    try expect(slice.len == 3);
}

test "single-item pointers do not index" {
    // A `*T` is one item. `ptr[1]` is a compile error, which is the whole
    // reason Zig distinguishes `*T` from `[*]T` where C has only `T*`.
    var x: u8 = 9;
    const single: *u8 = &x;
    try expect(single.* == 9);
}

C has one pointer type, T*, which is used for three different things: one item, many items, and “maybe nothing”. Zig splits them apart:

ZigMeaning
*Texactly one T
[*]Tmany T, count unknown
[]Tmany T, count known (a slice)
?*Tone T, or nothing

What [*]T gives up

A many-item pointer supports indexing and pointer arithmetic, but has no .len and therefore no bounds checking. It exists mainly for C interop and for implementing the safe abstractions on top.

The moment you know the length, convert to a slice:

const slice: []u8 = many[0..count];

That single step buys back bounds checks, for iteration, and .len. Prefer slices everywhere except the boundary where a length genuinely is not available.

Correspondingly, *T deliberately does not support indexing: ptr[1] on a single-item pointer is a compile error, catching a class of C bug at the type level.