Skip to editor content
learningzig.orglesson 10 of 25

Slices and Iteration

Slices are one of Zig's most important types. They provide a safe, fat-pointer view into arrays with both a pointer and a length, enabling flexible data access without copying.

Slices in Depth

A slice []T is a pointer and a length. It can reference any contiguous block of memory.

const std = @import("std");

pub fn main() !void {
    const numbers = [_]i32{ 10, 20, 30, 40, 50, 60, 70, 80 };

    // Slice the full array
    const all = numbers[0..];
    std.debug.print("All: ", .{});
    for (all) |n| std.debug.print("{} ", .{n});
    std.debug.print("(len={})\n", .{all.len});

    // Partial slices
    const first_three = numbers[0..3];
    const middle = numbers[2..6];
    const last_two = numbers[6..];

    std.debug.print("First 3: ", .{});
    for (first_three) |n| std.debug.print("{} ", .{n});
    std.debug.print("\n", .{});

    std.debug.print("Middle: ", .{});
    for (middle) |n| std.debug.print("{} ", .{n});
    std.debug.print("\n", .{});

    std.debug.print("Last 2: ", .{});
    for (last_two) |n| std.debug.print("{} ", .{n});
    std.debug.print("\n", .{});

    // Mutable slices
    var mutable = [_]i32{ 1, 2, 3, 4, 5 };
    const slice: []i32 = &mutable;
    slice[0] = 100;
    std.debug.print("Modified: {}\n", .{mutable[0]});
}

Sentinel-Terminated Slices

Some slices end with a special sentinel value, like null-terminated C strings.

const std = @import("std");

pub fn main() !void {
    // String literals are null-terminated slices
    const hello: [:0]const u8 = "Hello";
    std.debug.print("String: {s} (len={})\n", .{ hello, hello.len });

    // The sentinel is accessible at index len
    std.debug.print("Sentinel at [{}]: {}\n", .{ hello.len, hello[hello.len] });

    // Convert between slice types
    const plain: []const u8 = hello; // [:0] -> [] is safe
    std.debug.print("As plain slice: {s} (len={})\n", .{ plain, plain.len });

    // Iterate over bytes
    std.debug.print("Bytes: ", .{});
    for (hello) |byte| {
        std.debug.print("{} ", .{byte});
    }
    std.debug.print("\n", .{});
}

For Loop Patterns

Zig's for loops work with slices, arrays, and ranges.

const std = @import("std");

pub fn main() !void {
    const names = [_][]const u8{ "Alice", "Bob", "Charlie", "Diana" };

    // Basic iteration
    std.debug.print("Names: ", .{});
    for (names) |name| {
        std.debug.print("{s} ", .{name});
    }
    std.debug.print("\n", .{});

    // With index
    for (names, 0..) |name, i| {
        std.debug.print("  {}: {s}\n", .{ i, name });
    }

    // Multiple slices in parallel
    const scores = [_]i32{ 95, 87, 92, 88 };
    std.debug.print("\nScores:\n", .{});
    for (names, scores) |name, score| {
        std.debug.print("  {s}: {}\n", .{ name, score });
    }

    // Mutable iteration
    var values = [_]i32{ 1, 2, 3, 4, 5 };
    for (&values) |*v| {
        v.* *= 2;
    }
    std.debug.print("\nDoubled: ", .{});
    for (values) |v| std.debug.print("{} ", .{v});
    std.debug.print("\n", .{});
}

std.mem Utilities

The standard library provides functions for working with memory and slices.

const std = @import("std");

pub fn main() !void {
    const text = "Hello, World! Hello, Zig!";

    // Find substring
    if (std.mem.indexOf(u8, text, "World")) |pos| {
        std.debug.print("'World' found at position {}\n", .{pos});
    }

    // Count occurrences
    const count = std.mem.count(u8, text, "Hello");
    std.debug.print("'Hello' appears {} times\n", .{count});

    // Split
    std.debug.print("\nSplit by space:\n", .{});
    var iter = std.mem.splitScalar(u8, text, ' ');
    while (iter.next()) |word| {
        std.debug.print("  '{s}'\n", .{word});
    }

    // Equality
    const a = "hello";
    const b = "hello";
    const c = "world";
    std.debug.print("\n'hello' == 'hello': {}\n", .{std.mem.eql(u8, a, b)});
    std.debug.print("'hello' == 'world': {}\n", .{std.mem.eql(u8, a, c)});

    // Starts/ends with
    std.debug.print("starts with 'Hello': {}\n", .{std.mem.startsWith(u8, text, "Hello")});
    std.debug.print("ends with 'Zig!': {}\n", .{std.mem.endsWith(u8, text, "Zig!")});
}

Try It Yourself

const std = @import("std");

fn findMax(slice: []const i32) ?i32 {
    if (slice.len == 0) return null;
    var max = slice[0];
    for (slice[1..]) |val| {
        if (val > max) max = val;
    }
    return max;
}

fn findMin(slice: []const i32) ?i32 {
    if (slice.len == 0) return null;
    var min = slice[0];
    for (slice[1..]) |val| {
        if (val < min) min = val;
    }
    return min;
}

fn average(slice: []const i32) f64 {
    if (slice.len == 0) return 0;
    var sum: i64 = 0;
    for (slice) |val| sum += val;
    return @as(f64, @floatFromInt(sum)) / @as(f64, @floatFromInt(slice.len));
}

pub fn main() !void {
    const data = [_]i32{ 42, 17, 93, 56, 28, 71, 5, 88 };
    const slice: []const i32 = &data;

    std.debug.print("Data: ", .{});
    for (slice) |v| std.debug.print("{} ", .{v});
    std.debug.print("\n", .{});

    std.debug.print("Max: {}\n", .{findMax(slice).?});
    std.debug.print("Min: {}\n", .{findMin(slice).?});
    std.debug.print("Average: {d:.2}\n", .{average(slice)});

    // Slicing to find max of first half vs second half
    const mid = slice.len / 2;
    const first_half = slice[0..mid];
    const second_half = slice[mid..];
    std.debug.print("\nFirst half max: {}\n", .{findMax(first_half).?});
    std.debug.print("Second half max: {}\n", .{findMax(second_half).?});
}

Key Takeaways

  • Slices ([]T) are a pointer plus a length — safe and efficient
  • Sentinel-terminated slices ([:0]const u8) support C interop
  • for loops iterate over slices with optional index and parallel iteration
  • Use &values with |*v| for mutable iteration
  • std.mem provides splitting, searching, counting, and comparison utilities
  • Slicing is zero-cost — it creates a new view without copying data

Pro Tip: Prefer slices over arrays in function parameters. A function that takes []const i32 works with arrays of any size, other slices, and dynamically allocated memory. This makes your functions more reusable without any runtime cost.

Next Steps

Now that you can work with slices and iterate over data, it's time to see how Zig models concurrency -- the std.Io interface, launching tasks, awaiting results, and cancelling work you no longer need.

Next lesson

Async I/O with std.Io

Learn Zig 0.16's async I/O model — thread Io through your code like an allocator, launch tasks with io.async, await futures, fan out with std.Io.Group, and cancel work you no longer need.

30 min