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]i32Read it as:
array of 2 rows, where each row is an array of 3 i32 valuesSo this array has:
2 rows
3 columns per row
6 total numbersReading the Type
The type:
[2][3]i32means:
[2] of [3] of i32The 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]i32The full grid has type:
[2][3]i32Accessing 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:
| Expression | Value |
|---|---|
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 6The 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 6The 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] = 6The 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] == 99Changing 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]i32Read it as:
pointer to a constant 2 by 3 array of i32 valuesThe 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 6The 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]u8Read it as:
array of 2 layers
each layer has 3 rows
each row has 4 u8 valuesYou access one value with three indexes:
const x = cube[1][2][3];That means:
layer 1
row 2
column 3For 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]i32This 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 dataFor 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:
| Row | Column | Flat Index |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 0 | 2 | 2 |
| 1 | 0 | 3 |
| 1 | 1 | 4 |
| 1 | 2 | 5 |
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 6This mental model helps:
[rows][columns]TSo:
[2][3]i32means:
2 rows, 3 columnsCommon Mistake: Expecting a Slice of Slices
A value of type:
[2][3]i32is not the same as:
[][]i32A 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 sliceThey 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 0This 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 valuesSummary
A multidimensional array is an array of arrays.
[2][3]i32means 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.