Arrays and Structs
In this lesson, we'll explore Zig's data structures. You'll learn how to work with arrays, slices, and structs to organize and manage your data effectively.
What You'll Learn
- How to create and use arrays in Zig
- The difference between arrays and slices
- How to define and use structs
- Methods and namespaced functions
- Working with nested data structures
- Common patterns for data organization
Arrays
Arrays in Zig have a fixed size known at compile time:
const std = @import("std");
pub fn main() !void {
// Array with explicit size and type
const numbers: [5]i32 = [5]i32{1, 2, 3, 4, 5};
// Array with inferred size (use [_])
const fruits = [_][]const u8{"apple", "banana", "cherry"};
// Access elements
std.debug.print("First number: {}\n", .{numbers[0]});
std.debug.print("Second fruit: {s}\n", .{fruits[1]});
// Array length
std.debug.print("Array length: {}\n", .{numbers.len});
// Initialize array with same value
const zeros = [_]i32{0} ** 10; // Array of 10 zeros
std.debug.print("Zeros length: {}\n", .{zeros.len});
}
Array Operations
Work with arrays using loops and common patterns:
const std = @import("std");
pub fn main() !void {
var numbers = [_]i32{10, 20, 30, 40, 50};
// Modify array elements
numbers[0] = 15;
std.debug.print("Modified first element: {}\n", .{numbers[0]});
// Iterate over array
for (numbers) |num| {
std.debug.print("{} ", .{num});
}
std.debug.print("\n", .{});
// Iterate with index
for (numbers, 0..) |num, i| {
std.debug.print("[{}]: {} ", .{i, num});
}
std.debug.print("\n", .{});
// Sum array elements
var sum: i32 = 0;
for (numbers) |num| {
sum += num;
}
std.debug.print("Sum: {}\n", .{sum});
}
Slices
Slices are views into arrays with runtime-known length:
const std = @import("std");
pub fn main() !void {
const numbers = [_]i32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Create a slice (pointer + length)
const slice1 = numbers[0..5]; // Elements 0-4
const slice2 = numbers[5..]; // Elements 5 to end
const slice3 = numbers[0..]; // Entire array as slice
std.debug.print("First slice: ", .{});
for (slice1) |num| {
std.debug.print("{} ", .{num});
}
std.debug.print("\n", .{});
std.debug.print("Second slice: ", .{});
for (slice2) |num| {
std.debug.print("{} ", .{num});
}
std.debug.print("\n", .{});
// Slice length
std.debug.print("Slice1 length: {}\n", .{slice1.len});
// String is a slice of bytes
const message: []const u8 = "Hello, Zig!";
std.debug.print("Message: {s}\n", .{message});
std.debug.print("Message length: {}\n", .{message.len});
// Try creating your own slices!
}
Structs
Structs group related data together:
const std = @import("std");
// Define a struct
const Person = struct {
name: []const u8,
age: u32,
height: f32,
// Method (function inside struct)
pub fn introduce(self: Person) void {
std.debug.print("Hi, I'm {s}, {} years old, {d:.2}m tall\n",
.{self.name, self.age, self.height});
}
// Associated function (no self parameter)
pub fn create(name: []const u8, age: u32, height: f32) Person {
return Person{
.name = name,
.age = age,
.height = height,
};
}
};
pub fn main() !void {
// Create struct instance
const person1 = Person{
.name = "Alice",
.age = 30,
.height = 1.75,
};
// Access fields
std.debug.print("Name: {s}\n", .{person1.name});
std.debug.print("Age: {}\n", .{person1.age});
// Call method
person1.introduce();
// Use associated function
const person2 = Person.create("Bob", 25, 1.80);
person2.introduce();
}
Struct Patterns
Common patterns for working with structs:
const std = @import("std");
const Rectangle = struct {
width: f32,
height: f32,
// Calculate area
pub fn area(self: Rectangle) f32 {
return self.width * self.height;
}
// Calculate perimeter
pub fn perimeter(self: Rectangle) f32 {
return 2 * (self.width + self.height);
}
// Check if square
pub fn isSquare(self: Rectangle) bool {
return self.width == self.height;
}
// Resize
pub fn scale(self: Rectangle, factor: f32) Rectangle {
return Rectangle{
.width = self.width * factor,
.height = self.height * factor,
};
}
};
pub fn main() !void {
const rect = Rectangle{
.width = 10.0,
.height = 5.0,
};
std.debug.print("Area: {d:.2}\n", .{rect.area()});
std.debug.print("Perimeter: {d:.2}\n", .{rect.perimeter()});
std.debug.print("Is square: {}\n", .{rect.isSquare()});
const scaled = rect.scale(2.0);
std.debug.print("Scaled width: {d:.2}\n", .{scaled.width});
}
Nested Structs
Structs can contain other structs:
const std = @import("std");
const Address = struct {
street: []const u8,
city: []const u8,
zip: []const u8,
};
const Employee = struct {
name: []const u8,
id: u32,
address: Address,
pub fn displayInfo(self: Employee) void {
std.debug.print("Employee: {s} (ID: {})\n", .{self.name, self.id});
std.debug.print("Address: {s}, {s} {s}\n",
.{self.address.street, self.address.city, self.address.zip});
}
};
pub fn main() !void {
const employee = Employee{
.name = "Alice Smith",
.id = 12345,
.address = Address{
.street = "123 Main St",
.city = "Springfield",
.zip = "12345",
},
};
employee.displayInfo();
}
Arrays of Structs
Combine arrays and structs for collections of data:
const std = @import("std");
const Point = struct {
x: f32,
y: f32,
pub fn distance(self: Point, other: Point) f32 {
const dx = self.x - other.x;
const dy = self.y - other.y;
return @sqrt(dx * dx + dy * dy);
}
pub fn display(self: Point) void {
std.debug.print("({d:.1}, {d:.1})", .{self.x, self.y});
}
};
pub fn main() !void {
// Array of points
const points = [_]Point{
Point{ .x = 0.0, .y = 0.0 },
Point{ .x = 3.0, .y = 4.0 },
Point{ .x = 1.0, .y = 1.0 },
};
// Display all points
for (points, 0..) |point, i| {
std.debug.print("Point {}: ", .{i});
point.display();
std.debug.print("\n", .{});
}
// Calculate distances
const origin = points[0];
std.debug.print("\nDistances from origin:\n", .{});
for (points[1..], 1..) |point, i| {
const dist = origin.distance(point);
std.debug.print("Point {}: {d:.2}\n", .{i, dist});
}
// Try adding more points or methods!
}
Packed Structs
Packed structs have guaranteed memory layout:
const std = @import("std");
// Packed struct - guaranteed memory layout
const Flags = packed struct {
is_active: bool,
is_admin: bool,
is_verified: bool,
_padding: u5 = 0, // Pad to 8 bits
};
pub fn main() !void {
const flags = Flags{
.is_active = true,
.is_admin = false,
.is_verified = true,
};
std.debug.print("Active: {}\n", .{flags.is_active});
std.debug.print("Admin: {}\n", .{flags.is_admin});
std.debug.print("Verified: {}\n", .{flags.is_verified});
std.debug.print("Size: {} bytes\n", .{@sizeOf(Flags)});
}
Practice Exercises
Try these exercises:
- Create an array of your favorite numbers and calculate their average
- Define a
Bookstruct with title, author, and pages. Create an array of books - Write a struct method that modifies the struct (returns a new instance)
- Create a
Circlestruct with methods for area and circumference - Build a
Studentstruct that contains an array of test scores and a method to calculate GPA - Create a slice from an array and modify elements through the slice
Challenge: Create a TodoList struct that contains an array of Task structs (each with description and completed status). Add methods to mark tasks complete and count remaining tasks.
Key Takeaways
- Arrays have compile-time known size; slices have runtime-known size
- Use
[_]for array size inference and[start..end]for slicing - Structs group related data and can contain methods
- Methods take
selfas first parameter; associated functions don't - Structs can be nested and stored in arrays
- Packed structs provide guaranteed memory layout for low-level programming
Next Steps
Congratulations on completing the basics of Zig! You now understand:
- Variables and types
- Functions and error handling
- Control flow with conditionals and loops
- Data structures with arrays and structs
To continue learning:
- Explore Zig's memory management and allocators
- Learn about error unions and optional types in depth
- Study comptime programming for metaprogramming
- Practice building small projects to solidify your knowledge
Keep coding and exploring Zig's powerful features!