Arrays
Arrays are a fundamental data type for grouping multiple values of the same type together.
Creating Arrays
You can create arrays using syntax similar to languages like C and Go. You must specify the array’s length (number of elements) and the data type of its elements. The length is part of the array’s type.
const nums = [6]u8{4, 8, 15, 16, 23, 42};
This creates a stack-allocated array of six 8-bit integers. The array type is [6]u8
.
We can also let the Zig compiler figure out the number of elements in an array. This is done with the special _
keyword.
const temps = [_]f32{68.0, 56.4, 98.5};
The compiler will infer the temps
array type as [3]f32
.
Crucially, arrays in Zig have a fized size determined at compile time. They are static. This is because the array’s size dictates the amount of memory allocated for it, and this allocation happens when the program is compiled (or when the stack frame is created for local variables).
Selecting Elements
Zig arrays use zero-based indexing, meaning the first element is at index 0, the second at index 1, and so on.
Accessing Single Elements
You can access individual elements using square brackets with the desired index ([index]
). The type you get back will be the type of the element, which is f32
in the below example.
const temps = [_]f32{68.0, 56.4, 98.5};
const first = temps[0];
You can get the last index of an array by using its length:
const last = temps[temps.len - 1];
Accessing an index outside the bounds of the array (like temps[3]
) results in different behavior depending on your build mode. It is undefined behavior in ReleaseSmall
and ReleaseFast
, which can lead to crashes or incorrect results. In Debug
and ReleaseSafe
modes, it will typically trigger a panic.
Creating Slices
You can create a slice that refers to a portion (or all) of an array using the [start..end]
syntax, where end
is exclusive. A slice stores its own length and a pointer to the start of its sequence. We will discuss more about slices in the following section.
const nums = [6]u8{4, 8, 15, 16, 23, 42};
const slice = nums[1..4];
slice
is []u8{8, 15, 16}
and has a type of []u8
, meaning a slice of u8
.
Shorthands For Slicing
- Omitting
end
will slice fromstart
to the end of the original array:const slice_to_end = nums[2..]; // []u8{15, 16, 23, 42}
- Omitting
start
will slice from the beginning up to, but not including,end
:const slice_from_start = nums[..3]; // []u8{4, 8, 15}
- You can also slice the entire array:
const full_slice = nums[..]; // []u8{4, 8, 15, 16, 23, 42}
Slices
When you select a portion of an array using the start..end
syntax, you are slicing the array.
A slice is a simple but powerful data structure, represented internally by two components:
- Pointer (
ptr
): A pointer to the first element the slice refers to. - Length (
len
): Ausize
value indicating the number of elements in the slice.
const array = [_]u8{10, 20, 30, 40, 50};
const slice = array[1..4];
slice
stores the slice []u8{20, 30, 40}
. Internally, it stores a pointer to the address of array[1]
, where 20 is stored. It’s length property is set to 3.
Important Characteristics
Slices do not “own” the values they contain. They merely provide a view into a portion of the original array, meaning they do not own the underlying memory. If the slice was created from an array, modifying the slice will modify the array.
Additionally, the len
field storing the slice length is a runtime value. It is crucial for iteration and bounds checking. When you access an element using slice[index]
:
- In
Debug
andReleaseSafe
modes, a runtime check comparesindex
againstlen
to ensure it’s within bounds (0 <= index < len). Accessing an out-of-bounds index triggers a panic. - In
ReleaseFast
andReleaseSmall
modes, these runtime checks are typically omitted for performance, making out-of-bounds access undefined behavior. - The compiler can perform compile time bounds checks if both the index and length are known at compile time.
This example demonstrates accessing the runtime length:
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const temps = [_]f32{ 68.0, 56.4, 98.5, 22.3 };
const slice = temps[1..];
try stdout.print("slice length: {d}\n", .{slice.len});
}
Slice Length: Compile Time vs Runtime
It’s important to understand the difference between compile time and runtime, especially with arrays and slices.
- Compile time is the phase when the Zig compiler analyzes your source code, checks types, resolves
comptime
values, and generates the executable program. Values known at compile time are fixed constants embedded within the program before it runs. - Runtime is the phase when the compiled program is actually executed on the computer. Values determined or changed during runtime can depend on things like the program’s flow, user input, or calculations.
Array lengths are compile-time values because they are either explicitly defined or computed by the compiler. They are part of the array type, such as [5]u8
.
Slice lengths, on the other hand, are typically runtime values. This is because slices frequently refer to portions of data whose size isn’t fixed until code execution. Consider this example:
fn create_slice(arr: [4]u8, count: usize) []u8 {
var slice = arr[0..count];
return slice;
}
The slice length will not be known at compile time because it depends on count
, a variable whose value will be determined when create_slice()
is called. Until it’s called, there is no way for the compiler to know the value of count
so it is unable to determine the length of slice
.
Slice lengths can be known at compile time in specific cases, such as when slicing with constant indices or when using Zig’s comptime
keyword. If you slice an array (or another compile-time known data structure) using indices that are constants known at compile time, the resulting slice’s length is also known at compile time.
const arr = [4]u8{5, 10, 15, 20}; // arr.len = 4 (compile time)
const start_index = 1; // Constant
const end_index = 4; // Constant
// Both array and indices are compile-time known
const slice = arr[start_index..end_index]; // []u8{10, 15, 20}
During compilation, the compiler calculates slice.len = end_index - start_index
(4 - 1 = 3).
If the slice itself, or the values used to create it (like the start/end indices), are explicitly evaluated at compile time using the comptime
keyword, the length will be known then.
const arr = [_]u8{5, 10, 15, 20};
comptime var end_idx: usize = 3;
const slice = arr[0..end_idx]; // Evaluated at compile time
// slice.len = 3 - 0 = 3 (compile time)
We will explore Zig’s powerful comptime
feature in more detail later.
Slice vs Array Pointers
We learned that you can always access the underlying pointer of a slice using .ptr
. Assuming the slice isn’t empty, you can always dereference this pointer with the .*
operator to get the first element’s value. The .*
operator works on the pointer (slice.ptr
) directly, not on the slice variable itself.
If, and only if, a slice’s length N
is known at compile time, Zig allows that slice to be implicitly converted (or explicitly cast) to a pointer to an array of that specific size (*[N]T)
. You could then dereference it to get a copy of the array value or use the pointer to access the array’s elements.
const array = [_]u8{10, 20, 30, 40};
// len (3) known at compile time
const comptime_len_slice = array[1..4];
// Implicit conversion to array pointer
var array_ptr: *[3]u8 = comptime_len_slice;
// You can use array_ptr as a pointer to an array of 3 elements
// Dereference to get array copy
const the_array_value: [3]u8 = array_ptr.*; // {20, 30, 40}
// Or access elements via the array pointer
const second_element = array_ptr[1]; // 30
If a slice’s length is only known at runtime, this conversion to a fixed-size array pointer (*[N]T)
is not possible because the compiler doesn’t know what N
should be. This is represented below with ?
and is not valid code.
fn process(runtime_slice: []u8) void {
// ERROR: Cannot convert slice with runtime length to array pointer
var array_ptr: *[?]u8 = runtime_slice;
// Accessing .ptr is still fine
if (runtime_slice.len > 0) {
var first = runtime_slice.ptr.*;
// ...
}
}
In summary, slices with compile-time known lengths have the special ability to be treated as pointers to fixed-size arrays (*[N]T
). Slices with runtime lengths do not.
Array Operators
Zig provides operators for working with arrays, each with specific requirements for their operands. These requirements are detailed in the sections below. For more information, refer to Zig’s official documentation.
Concatenation (++
)
The ++
operator creates a new array by joining the elements of two existing arrays. Both operand arrays must have lengths known at compile time.
const a = [_]u8{1, 2};
const b = [_]u8{3, 4, 5};
const c = a ++ b;
try stdout.print("{any}\n", .{c}); // { 1, 2, 3, 4, 5 }
Repetition (**
)
The **
operator creates a new array by repeating the elements of an existing array a specified number of times. The array operand must have a length known at compile time, and the repetition count must also be a compile-time known integer (typed comptime_int
).
const a = [_]u8{1, 2};
const repeat_count = 2; // Compile-time known integer
const c = a ** repeat_count;
try stdout.print("{any}\n", .{c}); // { 1, 2, 1, 2 }
Note: Runtime Operations These operators (
++
,**
) do not work with slices where the length is only known at runtime. For runtime concatenation or repetition, you would typically use structures likestd.ArrayList
or manual memory allocation and copying. More on this later.
Equality (==
, !=
)
If two arrays have the exact same type ([N]T
), you can compare them for equality or inequality. Having the same type means they must have the same compile-time known length N
and the same element type T
.
const a = [_]u8{1, 2, 3}; // [3]u8
const b = [_]u8{1, 2, 3}; // [3]u8
const c = [_]u8{1, 2, 4}; // [3]u8
const d = [_]u8{1, 2}; // [2]u8
std.debug.print("a == b: {}\n", .{a == b}); // true
std.debug.print("a != c: {}\n", .{a != c}); // true
// Compile Error: type mismatch '[3]u8' != '[2]u8'
std.debug.print("a == d: {}\n", .{a == d});
Assignment (=
)
If two arrays have the exact same type ([N]T
), you can assign one array to another. Assignment performs a copy of the entire array’s contents from the right-hand side to the left-hand side.
var a = [_]u8{1, 2, 3}; // [3]u8
const b = [_]u8{4, 5, 6}; // [3]u8
const c = [_]u8{7, 8}; // [2]u8
a = b; // Copies contents of b into a
a = c; // Compile Error: type mismatch '[3]u8' != '[2]u8'
std.debug.print("a: {any}\n", .{a}); // a: { 4, 5, 6 }