Zig Arrays and Slices
Arrays and slices are core data structures for handling sequential data in Zig. This chapter will cover their usage and best practices in detail.
Array Basics
Array Declaration and Initialization
zig
const std = @import("std");
pub fn main() void {
// Array with explicit size
const explicit_array: [5]i32 = [5]i32{ 1, 2, 3, 4, 5 };
// Array with inferred size
const inferred_array = [_]i32{ 10, 20, 30, 40, 50 };
// Repeated initialization
const repeated_array = [_]i32{42} ** 8;
// Partial initialization (rest are zero values)
const partial_array = [10]i32{ 1, 2, 3 };
std.debug.print("Explicit array: ");
for (explicit_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("Inferred array: ");
for (inferred_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("Repeated array: ");
for (repeated_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("Partial initialization: ");
for (partial_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
}Array Properties and Operations
zig
const std = @import("std");
pub fn main() void {
var numbers = [_]i32{ 1, 2, 3, 4, 5 };
// Array length
std.debug.print("Array length: {}\n", .{numbers.len});
// Array element access
std.debug.print("First element: {}\n", .{numbers[0]});
std.debug.print("Last element: {}\n", .{numbers[numbers.len - 1]});
// Modify array element
numbers[2] = 100;
std.debug.print("Modified array: ");
for (numbers) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Array comparison
const array1 = [_]i32{ 1, 2, 3 };
const array2 = [_]i32{ 1, 2, 3 };
const array3 = [_]i32{ 1, 2, 4 };
std.debug.print("array1 == array2: {}\n", .{std.mem.eql(i32, &array1, &array2)});
std.debug.print("array1 == array3: {}\n", .{std.mem.eql(i32, &array1, &array3)});
}Multidimensional Arrays
zig
const std = @import("std");
pub fn main() void {
// 2D array
const matrix = [3][3]i32{
[3]i32{ 1, 2, 3 },
[3]i32{ 4, 5, 6 },
[3]i32{ 7, 8, 9 },
};
std.debug.print("3x3 Matrix:\n");
for (matrix, 0..) |row, i| {
std.debug.print("Row {}: ", .{i});
for (row) |item| {
std.debug.print("{:2} ", .{item});
}
std.debug.print("\n");
}
// 3D array
const cube = [2][2][2]i32{
[2][2]i32{
[2]i32{ 1, 2 },
[2]i32{ 3, 4 },
},
[2][2]i32{
[2]i32{ 5, 6 },
[2]i32{ 7, 8 },
},
};
std.debug.print("\n2x2x2 Cube:\n");
for (cube, 0..) |layer, i| {
std.debug.print("Layer {}:\n", .{i});
for (layer, 0..) |row, j| {
std.debug.print(" Row {}: ", .{j});
for (row) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
}
}
}Slice Basics
Slice Creation and Usage
zig
const std = @import("std");
pub fn main() void {
var array = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Create slices
const full_slice: []i32 = array[0..]; // Full slice
const partial_slice: []i32 = array[2..7]; // Partial slice
const end_slice: []i32 = array[5..]; // From index 5 to end
const start_slice: []i32 = array[0..5]; // From start to index 4
std.debug.print("Original array: ");
for (array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("Full slice: ");
for (full_slice) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("Partial slice [2..7]: ");
for (partial_slice) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("End slice [5..]: ");
for (end_slice) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("Start slice [0..5]: ");
for (start_slice) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
}Slice Properties and Operations
zig
const std = @import("std");
pub fn main() void {
var data = [_]u8{ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
var slice: []u8 = data[2..8];
// Slice properties
std.debug.print("Slice length: {}\n", .{slice.len});
std.debug.print("Slice pointer: {*}\n", .{slice.ptr});
// Slice content
std.debug.print("Slice content: ");
for (slice) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Modify slice (affects original array)
slice[0] = 99;
slice[slice.len - 1] = 88;
std.debug.print("Original array after modifying slice: ");
for (data) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Sub-slice
const sub_slice = slice[1..4];
std.debug.print("Sub-slice: ");
for (sub_slice) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
}Strings as Slices
String Slice Operations
zig
const std = @import("std");
pub fn main() void {
const text = "Hello, Zig Programming!";
// Strings are slices of type []const u8
std.debug.print("Full string: {s}\n", .{text});
std.debug.print("String length: {}\n", .{text.len});
// String slicing
const hello = text[0..5];
const zig = text[7..10];
const programming = text[11..22];
std.debug.print("Hello: {s}\n", .{hello});
std.debug.print("Zig: {s}\n", .{zig});
std.debug.print("Programming: {s}\n", .{programming});
// Character access
std.debug.print("First character: {c} (ASCII: {})\n", .{ text[0], text[0] });
std.debug.print("Last character: {c} (ASCII: {})\n", .{ text[text.len - 1], text[text.len - 1] });
// Iterate through characters
std.debug.print("Character by character: ");
for (text) |char| {
std.debug.print("{c}", .{char});
}
std.debug.print("\n");
}String Processing Functions
zig
const std = @import("std");
// Find substring
fn findSubstring(haystack: []const u8, needle: []const u8) ?usize {
if (needle.len > haystack.len) return null;
for (haystack, 0..) |_, i| {
if (i + needle.len > haystack.len) break;
if (std.mem.eql(u8, haystack[i..i + needle.len], needle)) {
return i;
}
}
return null;
}
// Count character occurrences
fn countChar(text: []const u8, char: u8) usize {
var count: usize = 0;
for (text) |c| {
if (c == char) count += 1;
}
return count;
}
// Reverse string
fn reverseString(allocator: std.mem.Allocator, text: []const u8) ![]u8 {
var reversed = try allocator.alloc(u8, text.len);
for (text, 0..) |char, i| {
reversed[text.len - 1 - i] = char;
}
return reversed;
}
pub fn main() !void {
const text = "Hello, Zig! Zig is great!";
// Find substring
if (findSubstring(text, "Zig")) |index| {
std.debug.print("Found 'Zig' at position: {}\n", .{index});
}
// Count character occurrences
const count = countChar(text, 'g');
std.debug.print("Character 'g' appears {} times\n", .{count});
// Reverse string
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const reversed = try reverseString(allocator, "Hello");
defer allocator.free(reversed);
std.debug.print("'Hello' reversed: {s}\n", .{reversed});
}Memory Layout of Arrays and Slices
Memory Contiguity
zig
const std = @import("std");
pub fn main() void {
var array = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("Array element addresses:\n");
for (array, 0..) |*item, i| {
std.debug.print("array[{}] = {} (address: {*})\n", .{ i, item.*, item });
}
// Verify memory contiguity
const ptr0 = @intFromPtr(&array[0]);
const ptr1 = @intFromPtr(&array[1]);
const element_size = @sizeOf(i32);
std.debug.print("\nMemory contiguity verification:\n");
std.debug.print("Element size: {} bytes\n", .{element_size});
std.debug.print("Address difference: {} bytes\n", .{ptr1 - ptr0});
std.debug.print("Memory contiguous: {}\n", .{ptr1 - ptr0 == element_size});
// Slice memory layout
const slice: []i32 = array[1..4];
std.debug.print("\nSlice information:\n");
std.debug.print("Slice pointer: {*}\n", .{slice.ptr});
std.debug.print("Slice length: {}\n", .{slice.len});
std.debug.print("Slice size: {} bytes\n", .{@sizeOf(@TypeOf(slice))});
}Dynamic Arrays (ArrayList)
ArrayList Basic Usage
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Create dynamic array
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
// Add elements
try list.append(10);
try list.append(20);
try list.append(30);
std.debug.print("Initial list: ");
for (list.items) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Insert element
try list.insert(1, 15);
std.debug.print("After inserting 15: ");
for (list.items) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Remove element
_ = list.orderedRemove(2);
std.debug.print("After removing index 2: ");
for (list.items) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Batch append
try list.appendSlice(&[_]i32{ 40, 50, 60 });
std.debug.print("After batch append: ");
for (list.items) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
std.debug.print("List length: {}\n", .{list.items.len});
std.debug.print("List capacity: {}\n", .{list.capacity});
}ArrayList Advanced Operations
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var numbers = std.ArrayList(i32).init(allocator);
defer numbers.deinit();
// Pre-allocate capacity
try numbers.ensureTotalCapacity(100);
std.debug.print("Pre-allocated capacity: {}\n", .{numbers.capacity});
// Batch initialization
for (0..10) |i| {
try numbers.append(@intCast(i * 10));
}
// Find element
const target = 50;
var found_index: ?usize = null;
for (numbers.items, 0..) |item, i| {
if (item == target) {
found_index = i;
break;
}
}
if (found_index) |index| {
std.debug.print("Found {} at index {}\n", .{ target, index });
}
// Sort
std.mem.sort(i32, numbers.items, {}, comptime std.sort.asc(i32));
std.debug.print("After sorting: ");
for (numbers.items) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Reverse
std.mem.reverse(i32, numbers.items);
std.debug.print("After reversing: ");
for (numbers.items) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
// Clear but retain capacity
numbers.clearRetainingCapacity();
std.debug.print("After clearing - length: {}, capacity: {}\n", .{ numbers.items.len, numbers.capacity });
}Array and Slice Algorithms
Search Algorithms
zig
const std = @import("std");
// Linear search
fn linearSearch(comptime T: type, array: []const T, target: T) ?usize {
for (array, 0..) |item, i| {
if (item == target) return i;
}
return null;
}
// Binary search (requires sorted array)
fn binarySearch(comptime T: type, array: []const T, target: T) ?usize {
var left: usize = 0;
var right: usize = array.len;
while (left < right) {
const mid = left + (right - left) / 2;
if (array[mid] == target) {
return mid;
} else if (array[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return null;
}
pub fn main() void {
const unsorted_array = [_]i32{ 64, 34, 25, 12, 22, 11, 90, 88, 76, 50, 42 };
const sorted_array = [_]i32{ 11, 12, 22, 25, 34, 42, 50, 64, 76, 88, 90 };
// Linear search
const target1 = 22;
if (linearSearch(i32, &unsorted_array, target1)) |index| {
std.debug.print("Linear search: found {} at position {}\n", .{ target1, index });
}
// Binary search
const target2 = 42;
if (binarySearch(i32, &sorted_array, target2)) |index| {
std.debug.print("Binary search: found {} at position {}\n", .{ target2, index });
}
// Search for non-existent element
const target3 = 99;
if (linearSearch(i32, &unsorted_array, target3)) |index| {
std.debug.print("Found {} at position {}\n", .{ target3, index });
} else {
std.debug.print("Did not find {}\n", .{target3});
}
}Sorting Algorithms
zig
const std = @import("std");
// Bubble sort
fn bubbleSort(comptime T: type, array: []T) void {
const n = array.len;
for (0..n) |i| {
for (0..n - i - 1) |j| {
if (array[j] > array[j + 1]) {
const temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
// Selection sort
fn selectionSort(comptime T: type, array: []T) void {
const n = array.len;
for (0..n) |i| {
var min_idx = i;
for (i + 1..n) |j| {
if (array[j] < array[min_idx]) {
min_idx = j;
}
}
if (min_idx != i) {
const temp = array[i];
array[i] = array[min_idx];
array[min_idx] = temp;
}
}
}
// Insertion sort
fn insertionSort(comptime T: type, array: []T) void {
for (1..array.len) |i| {
const key = array[i];
var j: isize = @intCast(i - 1);
while (j >= 0 and array[@intCast(j)] > key) {
array[@intCast(j + 1)] = array[@intCast(j)];
j -= 1;
}
array[@intCast(j + 1)] = key;
}
}
pub fn main() void {
// Test bubble sort
var bubble_array = [_]i32{ 64, 34, 25, 12, 22, 11, 90 };
std.debug.print("Before bubble sort: ");
for (bubble_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
bubbleSort(i32, &bubble_array);
std.debug.print("After bubble sort: ");
for (bubble_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n\n");
// Test selection sort
var selection_array = [_]i32{ 64, 25, 12, 22, 11 };
std.debug.print("Before selection sort: ");
for (selection_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
selectionSort(i32, &selection_array);
std.debug.print("After selection sort: ");
for (selection_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n\n");
// Test insertion sort
var insertion_array = [_]i32{ 12, 11, 13, 5, 6 };
std.debug.print("Before insertion sort: ");
for (insertion_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
insertionSort(i32, &insertion_array);
std.debug.print("After insertion sort: ");
for (insertion_array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n");
}Best Practices for Arrays and Slices
1. Choose the Right Data Structure
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// ✅ Fixed size, known data -> use array
const fixed_data = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("Fixed array: {} elements\n", .{fixed_data.len});
// ✅ Dynamic size, frequent modifications -> use ArrayList
var dynamic_data = std.ArrayList(i32).init(allocator);
defer dynamic_data.deinit();
for (0..10) |i| {
try dynamic_data.append(@intCast(i));
}
std.debug.print("Dynamic array: {} elements\n", .{dynamic_data.items.len});
// ✅ Read-only access, partial data -> use slice
const slice_data: []const i32 = dynamic_data.items[2..7];
std.debug.print("Slice: {} elements\n", .{slice_data.len});
}2. Bounds Checking and Safe Access
zig
const std = @import("std");
fn safeGet(comptime T: type, array: []const T, index: usize) ?T {
if (index >= array.len) return null;
return array[index];
}
fn safeSet(comptime T: type, array: []T, index: usize, value: T) bool {
if (index >= array.len) return false;
array[index] = value;
return true;
}
pub fn main() void {
var data = [_]i32{ 10, 20, 30, 40, 50 };
// ✅ Safe access
if (safeGet(i32, &data, 2)) |value| {
std.debug.print("Safely got data[2] = {}\n", .{value});
}
if (safeGet(i32, &data, 10)) |value| {
std.debug.print("Won't execute: {}\n", .{value});
} else {
std.debug.print("Index 10 out of bounds\n", .{});
}
// ✅ Safe set
if (safeSet(i32, &data, 1, 99)) {
std.debug.print("Successfully set data[1] = 99\n", .{});
}
if (!safeSet(i32, &data, 10, 99)) {
std.debug.print("Failed to set data[10]: index out of bounds\n", .{});
}
}3. Memory Efficiency
zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// ✅ Pre-allocate capacity to avoid frequent reallocations
var efficient_list = std.ArrayList(i32).init(allocator);
defer efficient_list.deinit();
try efficient_list.ensureTotalCapacity(1000); // Pre-allocate
for (0..1000) |i| {
efficient_list.appendAssumeCapacity(@intCast(i)); // No capacity check needed
}
std.debug.print("Efficient list: {} elements, capacity: {}\n",
.{ efficient_list.items.len, efficient_list.capacity });
// ✅ Use slices to avoid unnecessary copying
const slice = efficient_list.items[100..200];
std.debug.print("Slice processing: {} elements\n", .{slice.len});
}Practical Application Example
Matrix Operations
zig
const std = @import("std");
const Matrix = struct {
data: []f64,
rows: usize,
cols: usize,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(allocator: std.mem.Allocator, rows: usize, cols: usize) !Self {
const data = try allocator.alloc(f64, rows * cols);
@memset(data, 0.0);
return Self{
.data = data,
.rows = rows,
.cols = cols,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.data);
}
pub fn get(self: *const Self, row: usize, col: usize) f64 {
return self.data[row * self.cols + col];
}
pub fn set(self: *Self, row: usize, col: usize, value: f64) void {
self.data[row * self.cols + col] = value;
}
pub fn print(self: *const Self) void {
for (0..self.rows) |i| {
for (0..self.cols) |j| {
std.debug.print("{d:6.2} ", .{self.get(i, j)});
}
std.debug.print("\n");
}
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var matrix = try Matrix.init(allocator, 3, 3);
defer matrix.deinit();
// Initialize matrix
for (0..3) |i| {
for (0..3) |j| {
matrix.set(i, j, @floatFromInt(i * 3 + j + 1));
}
}
std.debug.print("3x3 Matrix:\n");
matrix.print();
}Summary
This chapter covered Zig arrays and slices in detail:
- ✅ Array declaration, initialization, and operations
- ✅ Multidimensional array usage
- ✅ Slice creation and properties
- ✅ String handling as slices
- ✅ Memory layout and contiguity
- ✅ Dynamic arrays with ArrayList
- ✅ Search and sorting algorithms
- ✅ Best practices and practical applications
Arrays and slices are the most fundamental and important data structures in Zig. Mastering their usage is crucial for writing efficient Zig programs. In the next chapter, we'll learn about Zig structs and enums.