Zig: Arrays, Slices, Pointers

Home · Blog

20 February 2025

One surprising fact about Zig is that the language has six different types for working with arrays. In this article, I want to go through each of them and explain what they’re for, how they work, and how you can convert between them.

Arrays and slices

We’ll start with the array, which is a fixed-length sequence of values in memory. Here’s an example of an array of five integers:

var array = [5]u64{ 1, 2, 3, 4, 5 };

At runtime, it’ll look like this:

Each box represents 8 bytes (64 bit); the whole array takes up 5 * 8 = 40 bytes.

Arrays are important because they’re the basis for all the other types, but having the length as part of the type makes them pretty inflexible. Zig has slices as a type for variable-length sequences:

var slice: []u64 = &array;

A slice contains a pointer to the first element and the number of elements:

This will look familiar if you’re coming from Go or Rust, except Go’s slices also have a “capacity.”

Four more types?

So arrays are fixed-length, slices are variable-length. What do we need the other types for? The main reason is compatibility with C APIs.

Arrays in C correspond to arrays in Zig, but C doesn’t have slices; instead you use a pointer to the first element to refer to an array in memory, and you use pointer arithmetic to access the other elements. In Zig, a many-item pointer lets you do the same thing:

var mip: [*]u64 = &array;

As you can see, the type in the code is [*]u64 -- it’s not *u64, which would be a pointer to a single value of type u64, and wouldn’t allow indexing. At runtime, a many-item pointer is just a pointer:

The downside of just passing pointers around is that you need some way to keep track of the length. For strings, C solves this by adding an extra NUL byte (a byte with all bits zero) to mark the end of the string, making it a “zero-terminated string.” The same method can work for other types, for example an array of pointers with a null pointer to mark the end.

In Zig, this is called a sentinel-terminated array. We can create a sentinel-terminated array with this syntax:

var sta = [5:0]u64{ 1, 2, 3, 4, 5 };

The type [5:0]u64 means array of 5 elements of type u64, with a 0 value as the sentinel. The compiler will add the sentinel, so in the compiled program this is actually six values:

The sentinel can be any value. If we feel like it, we can create a 99-terminated array:

var sta99 = [5:99]u64{ 1, 2, 3, 4, 5 };

A C-style pointer to a sentinel-terminated array is called a sentinel-terminated pointer.

var stp: [*:0]u64 = &sta;

Finally, the sentinel-terminated slice is a slice where the data is also terminated by a sentinel value:

var sts: [:0]u64 = &sta;

Conversions

Having all these related types means we often need to convert from one to another.

In a nutshell, you use &array to go from an array to a pointer or slice, slice.ptr to go from a slice to a pointer, and the standard-library function std.mem.span to go from sentinel-terminated pointer to (sentined-terminated) slice. Converting a sentinel-terminated array to a regular array is done implicity; same for pointers and slices.

This diagram gives an overview: