📘 第03课:数组与切片

📋 本课目录固定数组 切片 指针 Sentinel-Terminated 多维数组 性能对比 练习

固定数组

Zig的数组是编译期确定长度的,长度是类型的一部分:[5]u8[10]u8是不同类型。

const std = @import("std");

pub fn main() !void {
    // 声明数组
    const arr1 = [5]i32{ 1, 2, 3, 4, 5 };
    const arr2 = [_]i32{ 10, 20, 30 };  // 自动推断长度

    // 零初始化
    var buf: [1024]u8 = 0;  // 全部填0
    var buf2: [64]u8 = undefined;

    // 重复初始化
    const ones = [10]u8{0xFF} ** 3;  // [0xFF] 重复3次

    // 访问和修改
    var nums = [_]i32{ 1, 2, 3 };
    nums[0] = 42;  // 修改
    const first = nums[0];  // 读取

    // 编译期已知边界的安全检查
    // nums[3] = 0;  // 编译错误!越界

    // 获取数组长度
    const len = nums.len;  // 3 (comptime已知)

    // 遍历
    for (arr1, 0..) |val, i| {
        std.debug.print("[{}] = {}\n", .{ i, val });
    }

    // 数组拼接(编译期)
    const combined = arr1 ++ arr2;

    // 数组指针解引用
    const ptr: *const [5]i32 = &arr1;
    std.debug.print("ptr[0] = {}\n", .{ptr[0]});
}

切片

切片是运行时确定长度的视图,包含指针和长度。相当于胖指针。

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // 从数组创建切片
    var arr = [10]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    const full: []i32 = &arr;          // 全数组切片
    const sub: []i32 = arr[2..7];     // [3,4,5,6,7]
    const head: []i32 = arr[0..3];    // [1,2,3]
    const tail: []i32 = arr[7..];      // [8,9,10]

    // 堆分配切片
    const heap_slice = try allocator.alloc(i32, 100);
    defer allocator.free(heap_slice);

    // 初始化
    @memset(heap_slice, 0);

    // 切片操作
    std.debug.print("len={}, ptr={*}\n", .{ sub.len, sub.ptr });

    // 切片重新切分
    const inner = sub[1..3];  // [4,5]

    // 切片作为函数参数(不像数组那样拷贝)
    printSlice(full);
}

fn printSlice(items: []const i32) void {
    for (items) |item| {
        std.debug.print("{} ", .{item});
    }
    std.debug.print("\n", .{});
}

指针

Zig有多种指针类型,每种都有明确语义:

const std = @import("std");

pub fn main() !void {
    var x: i32 = 42;

    // 单项指针
    const ptr1: *i32 = &x;       // 可变指针
    const ptr2: *const i32 = &x; // 只读指针

    // 解引用
    ptr1.* = 100;  // 通过指针修改
    std.debug.print("x = {}\n", .{ptr2.*});  // 100

    // 切片指针(最常用)
    var arr = [5]i32{ 1, 2, 3, 4, 5 };
    const slice_ptr: []i32 = &arr;

    // 指针算术(通过slice,不直接对指针做)
    const offset = slice_ptr[2..]; // 从第3个开始

    // 可选指针(可为null)
    var maybe_ptr: ?*i32 = &x;
    maybe_ptr = null;

    // C指针(与C互操作时使用)
    // var c_ptr: [*c]i32 = &x;

    _ = offset;
}
🚫 Zig指针安全规则:

Sentinel-Terminated

Zig支持sentinel-terminated数组和切片,最常见的是C字符串(以0结尾):

const std = @import("std");

pub fn main() !void {
    // Sentinel-terminated数组
    const c_str: [5:0]u8 = .{ 'h', 'e', 'l', 'l', 'o' };
    // 内存: [h, e, l, l, o, \0]  -- 6字节!

    // Sentinel-terminated切片
    const msg: [:0]const u8 = "hello";  // 字面量就是[:0]const u8

    // 与C互操作时传递
    const c_ptr: [*:0]const u8 = c_str;

    // 互转
    const regular_slice: []const u8 = c_str[0..5];  // 去掉sentinel
    const sentinel_slice: [:0]const u8 = std.mem.sliceTo(c_str, 0);

    // 自定义sentinel值
    const paths: [:'\n']const u8 = "/usr/bin";

    _ = regular_slice;
    _ = sentinel_slice;
    _ = paths;
}

多维数组

// 二维数组(行优先)
const matrix = [3][3]f32{
    .{ 1.0, 0.0, 0.0 },
    .{ 0.0, 1.0, 0.0 },
    .{ 0.0, 0.0, 1.0 },
};

// 矩阵乘法
fn matMul(comptime N: usize, a: [N][N]f32, b: [N][N]f32) [N][N]f32 {
    var result: [N][N]f32 = 0;
    for (0..N) |i| {
        for (0..N) |j| {
            for (0..N) |k| {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }
    return result;
}

性能对比

📊 数组遍历 1M 元素 (ms)

方式DebugReleaseFast
Zig for循环2.80.4
Zig while + 索引2.80.4
C for循环-0.4
Rust iter()-0.5
💡 ReleaseFast模式下Zig数组操作与C完全等价——零开销抽象。Debug模式的安全检查约7x开销。

练习

📝 课后练习

  1. 数组操作:实现一个函数reverse(arr: []i32)原地反转数组
  2. 矩阵转置:实现3×3矩阵转置,使用comptime参数泛化到N×N
  3. 字符串长度:不用std.mem.len,手写函数计算[:0]const u8的长度
  4. 切片去重:对一个有序切片去重,返回新长度
  5. sentinel实践:创建一个以0xFF为sentinel的u8数组,编写遍历函数