Skip to content

Fixed Arrays

An array is a group of values stored next to each other.

An array is a group of values stored next to each other.

In Zig, a fixed array has a length known at compile time. The length is part of the type.

const numbers = [3]i32{ 10, 20, 30 };

This creates an array with 3 values. Each value has type i32.

The type of numbers is:

[3]i32

Read this as:

array of 3 i32 values

The length is not just a detail. It belongs to the type. That means [3]i32 and [4]i32 are different types.

const a: [3]i32 = .{ 1, 2, 3 };
const b: [4]i32 = .{ 1, 2, 3, 4 };

These are not the same type.

Array Literals

You can write an array by giving its length and element type:

const letters = [4]u8{ 'a', 'b', 'c', 'd' };

Here:

[4]u8

means an array of 4 bytes.

Each item is a u8. Character literals such as 'a' are integer values. The byte value for 'a' is stored in the array.

You can also let Zig infer the length:

const letters = [_]u8{ 'a', 'b', 'c', 'd' };

The underscore means: “compiler, count the items for me.”

So this:

const letters = [_]u8{ 'a', 'b', 'c', 'd' };

has type:

[4]u8

For beginners, this is usually the best style when the array literal itself makes the length obvious.

Accessing Items

Array indexes start at 0.

const numbers = [_]i32{ 10, 20, 30 };

const first = numbers[0];
const second = numbers[1];
const third = numbers[2];

The first item is at index 0, not index 1.

So for this array:

const numbers = [_]i32{ 10, 20, 30 };

the indexes are:

IndexValue
010
120
230

Index 3 would be invalid, because the array has only 3 items.

const bad = numbers[3]; // invalid

In safe build modes, Zig checks array access and can catch invalid indexing.

Changing Array Items

If an array is declared with const, you cannot modify it.

const numbers = [_]i32{ 10, 20, 30 };

// numbers[0] = 99; // error

Use var when the array should be mutable:

var numbers = [_]i32{ 10, 20, 30 };

numbers[0] = 99;

Now the array contains:

99, 20, 30

The array length still cannot change. A fixed array always keeps the same number of items.

You may change the values, but you may not grow or shrink the array.

Getting the Length

Every array has a length.

const numbers = [_]i32{ 10, 20, 30 };

const len = numbers.len;

Here, numbers.len is 3.

Because fixed array length is known at compile time, Zig can use it for checks and optimizations.

You can loop over an array using its length:

const std = @import("std");

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

    var i: usize = 0;
    while (i < numbers.len) : (i += 1) {
        std.debug.print("{}\n", .{numbers[i]});
    }
}

This prints:

10
20
30

The type usize is commonly used for indexes and lengths. Its size depends on the target machine.

Looping with for

A for loop is usually simpler when you want to visit every item.

const std = @import("std");

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

    for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    }
}

This means: for each item in numbers, call it n, then run the loop body.

You can also get the index:

const std = @import("std");

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

    for (numbers, 0..) |n, i| {
        std.debug.print("numbers[{}] = {}\n", .{ i, n });
    }
}

This prints:

numbers[0] = 10
numbers[1] = 20
numbers[2] = 30

The 0.. creates a range starting from zero.

Arrays Are Values

In Zig, arrays are values.

That means assigning an array to another variable copies the whole array.

var a = [_]i32{ 1, 2, 3 };
var b = a;

b[0] = 99;

Now:

a = 1, 2, 3
b = 99, 2, 3

Changing b does not change a, because b is a copy.

This is important. If the array is large, copying it can be expensive. For large data, you often pass a slice or pointer instead. We will cover slices later.

Passing Arrays to Functions

A function can take an array as a parameter:

const std = @import("std");

fn printThree(numbers: [3]i32) void {
    for (numbers) |n| {
        std.debug.print("{}\n", .{n});
    }
}

pub fn main() void {
    const values = [_]i32{ 10, 20, 30 };
    printThree(values);
}

The function accepts exactly [3]i32.

This works:

printThree([_]i32{ 10, 20, 30 });

This does not work:

printThree([_]i32{ 10, 20, 30, 40 });

The second array has type [4]i32, but the function expects [3]i32.

This strictness is useful. The compiler knows exactly what size of array the function accepts.

Passing a Pointer to an Array

If you do not want to copy the whole array, pass a pointer:

const std = @import("std");

fn printThree(numbers: *const [3]i32) void {
    for (numbers.*) |n| {
        std.debug.print("{}\n", .{n});
    }
}

pub fn main() void {
    const values = [_]i32{ 10, 20, 30 };
    printThree(&values);
}

Here:

*const [3]i32

means:

pointer to a constant array of 3 i32 values

The &values expression gets the address of the array.

Inside the function:

numbers.*

means “the array that this pointer points to.”

For beginners, this syntax may look strange at first. The key idea is simple: passing a pointer avoids copying the whole array.

Array Repetition

Zig can create arrays by repeating a value.

const zeroes = [_]u8{0} ** 8;

This creates:

0, 0, 0, 0, 0, 0, 0, 0

The type is:

[8]u8

This is useful for initialization.

var buffer = [_]u8{0} ** 1024;

This creates a fixed buffer of 1024 bytes, all set to zero.

Nested Arrays

Arrays can contain arrays.

const matrix = [2][3]i32{
    .{ 1, 2, 3 },
    .{ 4, 5, 6 },
};

The type is:

[2][3]i32

Read it from left to right:

array of 2 arrays, each containing 3 i32 values

You can access items with two indexes:

const x = matrix[0][1];

This gets row 0, column 1, which is 2.

Arrays and Strings

A string literal in Zig has an array-like type.

const name = "zig";

This is not exactly a plain [3]u8. String literals include a sentinel value, which is useful for C interop and some low-level operations.

For now, think of a string literal as bytes that can be viewed as text:

const std = @import("std");

pub fn main() void {
    const name = "zig";

    for (name) |byte| {
        std.debug.print("{}\n", .{byte});
    }
}

This prints the byte values:

122
105
103

Those are the UTF-8 bytes for z, i, and g.

We will study strings in more detail later. The important point here is that Zig treats text as bytes, and arrays are one of the basic ways to store bytes.

Fixed Arrays vs Slices

A fixed array owns a known number of items.

[3]i32

A slice points to a sequence of items and carries a runtime length.

[]i32

A fixed array is good when the size is known and part of the design.

A slice is better when a function should work with many possible lengths.

For example, this function accepts exactly 3 items:

fn sumThree(numbers: [3]i32) i32 {
    return numbers[0] + numbers[1] + numbers[2];
}

This function accepts any number of items:

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

    for (numbers) |n| {
        total += n;
    }

    return total;
}

You can pass an array as a slice:

const values = [_]i32{ 10, 20, 30, 40 };

const result = sum(values[0..]);

The expression:

values[0..]

creates a slice covering the whole array.

When to Use Fixed Arrays

Use fixed arrays when the number of elements is known and stable.

Good examples:

3D vector with 3 coordinates
RGB color with 3 channels
small lookup table
temporary stack buffer
fixed protocol header
matrix with known dimensions

For example:

const rgb = [3]u8{ 255, 128, 0 };

This clearly means: exactly 3 bytes.

Use a slice when the length may vary:

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

The slice version is more flexible.

Summary

A fixed array stores a known number of values.

Its type includes both the length and the element type:

[3]i32

The length cannot change. If the array is mutable, the values can change, but the number of items stays fixed.

Arrays are values, so assigning or passing an array can copy the whole array. For large arrays, use a pointer or slice.

Fixed arrays are one of the simplest data structures in Zig, but they teach an important Zig habit: make size, ownership, and memory layout visible in the type.