Skip to content

Multidimensional Arrays

A multidimensional array is an array whose elements are also arrays.

A multidimensional array is an array whose elements are also arrays.

The most common example is a table, grid, or matrix.

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

The type is:

[2][3]i32

Read it as:

array of 2 rows, where each row is an array of 3 i32 values

So this array has:

2 rows
3 columns per row
6 total numbers

Reading the Type

The type:

[2][3]i32

means:

[2] of [3] of i32

The outer array has 2 elements. Each element is another array of 3 i32 values.

You can think of it like this:

grid
|
+-- row 0: [ 1, 2, 3 ]
+-- row 1: [ 4, 5, 6 ]

Each row has type:

[3]i32

The full grid has type:

[2][3]i32

Accessing Items

Use one index to get a row.

const row0 = grid[0];

Use two indexes to get one value.

const value = grid[1][2];

For this grid:

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

the indexes are:

ExpressionValue
grid[0][0]1
grid[0][1]2
grid[0][2]3
grid[1][0]4
grid[1][1]5
grid[1][2]6

The first index chooses the row. The second index chooses the column.

grid[row][column]

Indexes start at 0.

Changing Values

Use var if you want to change the array.

var grid = [2][3]i32{
    .{ 1, 2, 3 },
    .{ 4, 5, 6 },
};

grid[0][1] = 99;

Now the grid contains:

1   99   3
4    5   6

The shape cannot change. It is still a [2][3]i32.

You can change values, but you cannot add a third row or a fourth column.

Looping Over Rows

A simple for loop over the grid gives you one row at a time.

const std = @import("std");

pub fn main() void {
    const grid = [2][3]i32{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
    };

    for (grid) |row| {
        for (row) |value| {
            std.debug.print("{} ", .{value});
        }
        std.debug.print("\n", .{});
    }
}

Output:

1 2 3
4 5 6

The outer loop visits each row. The inner loop visits each value inside that row.

Looping With Indexes

Sometimes you need row and column numbers.

const std = @import("std");

pub fn main() void {
    const grid = [2][3]i32{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
    };

    for (grid, 0..) |row, r| {
        for (row, 0..) |value, c| {
            std.debug.print("grid[{}][{}] = {}\n", .{ r, c, value });
        }
    }
}

Output:

grid[0][0] = 1
grid[0][1] = 2
grid[0][2] = 3
grid[1][0] = 4
grid[1][1] = 5
grid[1][2] = 6

The outer index r is the row index. The inner index c is the column index.

Arrays Are Still Values

A multidimensional array is still an array value.

If you assign it to another variable, Zig copies the whole array.

var a = [2][3]i32{
    .{ 1, 2, 3 },
    .{ 4, 5, 6 },
};

var b = a;

b[0][0] = 99;

Now:

a[0][0] == 1
b[0][0] == 99

Changing b does not change a.

This is useful when you want an independent copy. It can be expensive if the array is large.

Passing Multidimensional Arrays to Functions

A function can accept a multidimensional array directly.

const std = @import("std");

fn printGrid(grid: [2][3]i32) void {
    for (grid) |row| {
        for (row) |value| {
            std.debug.print("{} ", .{value});
        }
        std.debug.print("\n", .{});
    }
}

pub fn main() void {
    const grid = [2][3]i32{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
    };

    printGrid(grid);
}

This function accepts exactly a [2][3]i32.

This works:

printGrid(.{
    .{ 1, 2, 3 },
    .{ 4, 5, 6 },
});

This does not work:

printGrid(.{
    .{ 1, 2 },
    .{ 3, 4 },
});

That second value has shape [2][2]i32, not [2][3]i32.

The shape is part of the type.

Passing by Pointer

Passing the whole array copies it. For small arrays this is fine. For large arrays, pass a pointer.

const std = @import("std");

fn printGrid(grid: *const [2][3]i32) void {
    for (grid.*) |row| {
        for (row) |value| {
            std.debug.print("{} ", .{value});
        }
        std.debug.print("\n", .{});
    }
}

pub fn main() void {
    const grid = [2][3]i32{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
    };

    printGrid(&grid);
}

The parameter type is:

*const [2][3]i32

Read it as:

pointer to a constant 2 by 3 array of i32 values

The function can read the grid but cannot modify it.

Passing a Mutable Pointer

To let a function modify the grid, use a mutable pointer.

fn clearGrid(grid: *[2][3]i32) void {
    for (grid) |*row| {
        for (row) |*value| {
            value.* = 0;
        }
    }
}

Usage:

var grid = [2][3]i32{
    .{ 1, 2, 3 },
    .{ 4, 5, 6 },
};

clearGrid(&grid);

After the call, every value is zero.

The |*row| syntax captures each row by pointer. The |*value| syntax captures each value by pointer. Then value.* = 0 writes through the pointer.

For beginners, the main idea is this: to modify elements inside a loop, you need access to the elements themselves, not just copies of them.

Row-Major Layout

Zig stores nested fixed arrays in a direct, predictable layout.

For:

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

the values are stored like this:

1 2 3 4 5 6

The first row comes first, then the second row.

This layout is called row-major order.

That means grid[0] is stored before grid[1].

This matters for performance. Programs usually run faster when they read memory in order.

Good:

for (grid) |row| {
    for (row) |value| {
        // reads values in memory order
    }
}

Less ideal:

var c: usize = 0;
while (c < 3) : (c += 1) {
    var r: usize = 0;
    while (r < 2) : (r += 1) {
        _ = grid[r][c];
    }
}

The second version reads by column. For small arrays, it does not matter. For large arrays, memory order can affect speed.

Three-Dimensional Arrays

You can nest more arrays.

const cube = [2][3][4]u8{
    .{
        .{ 1, 2, 3, 4 },
        .{ 5, 6, 7, 8 },
        .{ 9, 10, 11, 12 },
    },
    .{
        .{ 13, 14, 15, 16 },
        .{ 17, 18, 19, 20 },
        .{ 21, 22, 23, 24 },
    },
};

The type is:

[2][3][4]u8

Read it as:

array of 2 layers
each layer has 3 rows
each row has 4 u8 values

You access one value with three indexes:

const x = cube[1][2][3];

That means:

layer 1
row 2
column 3

For absolute beginners, two-dimensional arrays are enough for now. The same rule extends to more dimensions.

Fixed Shape vs Dynamic Shape

A multidimensional fixed array has a compile-time shape.

[2][3]i32

This means the number of rows and columns is known before the program runs.

This is useful for:

small matrices
game boards with fixed size
lookup tables
pixel kernels
protocol tables
embedded buffers
compile-time data

For example, a 3 by 3 image filter kernel can be stored as:

const kernel = [3][3]f32{
    .{ 0, -1, 0 },
    .{ -1, 5, -1 },
    .{ 0, -1, 0 },
};

A chess board could be:

const Board = [8][8]Piece;

The fixed shape makes the code precise.

But if the size is only known at runtime, a fixed multidimensional array is not the right tool. You will usually use a flat allocation plus width and height values, or a slice of rows.

Flattened Arrays

Sometimes a flat array is better than a multidimensional one.

Instead of:

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

you can store:

const width = 3;
const height = 2;

const grid = [_]i32{
    1, 2, 3,
    4, 5, 6,
};

To access row r, column c, compute the index:

const index = r * width + c;
const value = grid[index];

For a 2 by 3 grid:

RowColumnFlat Index
000
011
022
103
114
125

This layout is common in graphics, games, numerical code, and parsers.

The multidimensional form is clearer:

grid[r][c]

The flat form is often more flexible:

grid[r * width + c]

Common Mistake: Uneven Rows

This is invalid:

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

The first row has 3 values. The second row has 2 values.

But the type says every row must be [3]i32.

Each row must have the same length.

Common Mistake: Reversing Rows and Columns

In:

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

the type is [2][3]i32, not [3][2]i32.

There are 2 rows, and each row has 3 values.

This is valid:

row 0: 1 2 3
row 1: 4 5 6

This mental model helps:

[rows][columns]T

So:

[2][3]i32

means:

2 rows, 3 columns

Common Mistake: Expecting a Slice of Slices

A value of type:

[2][3]i32

is not the same as:

[][]i32

A fixed multidimensional array stores everything in one nested fixed structure.

A slice of slices is different. It stores a sequence of slices, and each inner slice may point somewhere else.

For beginners, keep this distinction simple:

[2][3]i32  fixed shape, stored directly
[][]i32    dynamic rows, each row is a slice

They are useful for different jobs.

A Complete Example

const std = @import("std");

fn sumGrid(grid: *const [2][3]i32) i32 {
    var total: i32 = 0;

    for (grid.*) |row| {
        for (row) |value| {
            total += value;
        }
    }

    return total;
}

fn clearGrid(grid: *[2][3]i32) void {
    for (grid) |*row| {
        for (row) |*value| {
            value.* = 0;
        }
    }
}

pub fn main() void {
    var grid = [2][3]i32{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
    };

    const total = sumGrid(&grid);
    std.debug.print("sum = {}\n", .{total});

    clearGrid(&grid);

    for (grid) |row| {
        for (row) |value| {
            std.debug.print("{} ", .{value});
        }
        std.debug.print("\n", .{});
    }
}

Output:

sum = 21
0 0 0
0 0 0

This example shows the main ideas:

use [2][3]i32 for a fixed 2 by 3 grid
pass *const [2][3]i32 to read without copying
pass *[2][3]i32 to modify without copying
loop over rows, then values

Summary

A multidimensional array is an array of arrays.

[2][3]i32

means an array of 2 rows, where each row contains 3 i32 values.

Use:

grid[row][column]

to access one value.

The shape is part of the type. [2][3]i32 and [3][2]i32 are different types.

Multidimensional arrays are best when the shape is known at compile time. They are clear, direct, and stored predictably in memory.