Skip to content

Day 12: 数组与切片

欢迎来到第十二天!在之前的学习中,我们已经接触了基本的数据类型。今天,我们将学习如何在Zig中处理一系列的同类型数据。Zig为此提供了两种主要的数据结构:数组(Arrays)和切片(Slices)。理解它们之间的区别与联系,是高效进行Zig编程的关键。数组是固定大小的内存块,而切片则为操作这些内存块(或其他连续内存区域)提供了动态的、灵活的视图。

数组是一个拥有固定、编译时已知大小的连续内存序列,其中包含相同类型的元素。因为大小是固定的,所以数组的大小是其类型信息的一部分。

声明与初始化:

// 声明一个包含4个i32的数组,所有元素初始化为0
var numbers: [4]i32 = .{ 0, 0, 0, 0 };
// 使用类型推断,并初始化值
const letters = [_]u8{ 'a', 'b', 'c' }; // 编译器推断大小为3
// 初始化所有元素为同一个值
const zeros = [_]u8{0} ** 100; // 一个包含100个0的数组
// 未初始化的数组
var uninitialized_array: [10]f32 = undefined;
  • [N]T 是数组的类型,N 是大小,T 是元素类型。
  • [_]T{...} 语法让编译器根据初始化列表中的元素数量自动推断数组的大小。
  • {value} ** N 是一种方便的语法,用于创建所有元素都相同的数组。

访问元素:

通过索引访问数组元素,索引从0开始。

var arr = [_]i32{ 10, 20, 30 };
arr[1] = 25; // 修改第二个元素
const value = arr[0]; // 读取第一个元素

切片是对数组(或任何连续内存区域)的一个“视图”或“引用”。它本身不拥有数据,而是指向一段内存。一个切片包含两个核心部分:一个指向其第一个元素的指针(ptr)和一个长度(len)。

切片的类型写作 []T,其中 T 是元素类型。与数组不同,切片的长度在编译时是未知的。

创建切片:

最常见的创建切片的方式是“切割”一个数组。

const arr = [_]i32{ 10, 20, 30, 40, 50 };
// 创建一个指向整个数组的切片
var slice_all: []i32 = &arr;
// 创建一个指向部分数组的切片
const slice_partial = arr[1..4]; // 包含索引1, 2, 3的元素,即 { 20, 30, 40 }
// 从头开始的切片
const slice_from_start = arr[0..3]; // or arr[..3]
// 直到结尾的切片
const slice_to_end = arr[2..]; // { 30, 40, 50 }
  • &arr 或 arr[0..] 或 arr[..] 都可以创建一个指向整个数组的切片。
  • 切片操作 [start..end] 是半开区间,即包含 start 索引,但不包含 end 索引。

Zig支持多维数组,即数组的数组。

// 一个 2x3 的二维数组
var matrix: [2][3]i32 = .{
.{ 1, 2, 3 },
.{ 4, 5, 6 },
};
matrix[1][1] = 0; // 访问第二行第二列的元素

你同样可以创建多维切片,例如 [][]i32。

Zig非常注重安全。在 Debug 和 ReleaseSafe 编译模式下,所有对数组和切片的访问都会进行边界检查。如果索引超出了 len 的范围,程序会触发一个panic,而不是像C语言那样导致未定义行为。

const arr = [_]i32{ 1, 2 };
const oops = arr[2]; // Panic! Index 2 out of bounds (len=2)

在 ReleaseFast 模式下,为了极致性能,这些检查会被禁用。

下面的函数接受一个切片,并原地反转其元素的顺序。

const std = @import("std");
fn reverse(slice: []anytype) void {
var i: usize = 0;
var j: usize = slice.len - 1;
while (i < j) {
const temp = slice[i];
slice[i] = slice[j];
slice[j] = temp;
i += 1;
j -= 1;
}
}
pub fn main() !void {
var numbers = [_]i32{ 1, 2, 3, 4, 5 };
reverse(&numbers);
std.debug.print("{any}\n", .{numbers}); // 输出: { 5, 4, 3, 2, 1 }
}

这个函数使用了 []anytype,使其可以接受任何类型的切片,展示了Zig泛型的初步应用。

7. 实践练习:实现一个初步的 ArrayList

Section titled “7. 实践练习:实现一个初步的 ArrayList”

尝试实现一个简化的 ArrayList 结构体。它需要一个分配器(std.mem.Allocator)来动态管理内存。

你需要实现以下结构和函数:

  • struct MyArrayList(T: type): 一个泛型结构体,包含 items: []T 和 capacity: usize 字段,以及一个指向分配器的指针。
  • init(allocator: *std.mem.Allocator) Self: 初始化一个新的、空的 ArrayList。
  • append(self: *Self, item: T) !void: 添加一个元素。如果容量不足,需要重新分配一个更大的内存块(例如,2倍大小),并将旧数据复制过去。
  • deinit(self: *Self): 释放 ArrayList 占用的所有内存。

这是一个挑战性的练习,会让你对切片、指针、内存分配和泛型有一个更深入的理解。

问:什么是悬垂切片(Dangling Slice)?

答:当一个切片指向的内存已经被释放或已经超出了作用域,这个切片就变成了悬垂切片。访问它会导致未定义行为。例如,一个函数创建了一个局部数组并返回了指向它的切片,这是非常危险的,因为函数返回后,局部数组的内存就会被回收。

fn get_dangling_slice() []u8 {
var array = [_]u8{1, 2, 3};
return &array; // 危险!返回了指向局部变量的切片
}

问:数组和切片作为函数参数有什么区别?

答:数组是按值传递的,这意味着整个数组会被复制一份。对于大数组来说,这会带来巨大的性能开销。切片也是按值传递的,但被复制的只是指针和长度(通常是16个字节),这非常高效。因此,函数参数应优先使用切片,除非你确实需要一个编译时固定大小的数组并且希望复制它。

今天,我们区分了Zig中的数组和切片。数组是固定大小的数据容器,而切片是操作这些数据或其他连续内存区域的动态、轻量级视图。我们学习了如何创建、初始化和操作它们,并了解了Zig在安全方面提供的边界检查。切片是Zig编程中使用得最广泛、最重要的复合类型之一。

明天,我们将专注于处理文本数据:字符串与字面量。