Ziglings 笔记 76: 哨兵 (Sentinels) - 寻找结束的标志
到此为止
在 Ziglings 的第 76 个练习中,我们学习了 Sentinels (哨兵)。
在很多系统中,我们不知道数据到底有多长,但我们知道数据“以什么结束”。最著名的例子就是 C 语言的字符串(以 0 结尾)。Zig 原生支持这种模式,并且不仅限于字符串,任何类型的数组都可以有哨兵。
挑战:同源不同相
我们有一个数组 nums,我们在其中间插入了一个 0。
nums是一个定长数组,以 0 为哨兵。ptr是一个指向nums的多项指针,也以 0 为哨兵。
当我们分别打印它们时,会发生什么?
解决方案
我们需要完善 printSequence 函数。主要修复点是添加打印时的空格,以及理解遍历逻辑。
const print = @import("std").debug.print;
const sentinel = @import("std").meta.sentinel;
pub fn main() void {
// 定义一个带哨兵的数组:长度为 6,以 0 结尾
// 内存布局:1, 2, 3, 4, 5, 6, 0 (编译器自动维护最后的 0)
var nums = [_:0]u32{ 1, 2, 3, 4, 5, 6 };
// 定义一个带哨兵的指针
const ptr: [*:0]u32 = &nums;
// 捣乱:把中间的值改成 0
// 现在的内存:1, 2, 3, 0, 5, 6, 0
nums[3] = 0;
// 打印数组:输出 1 2 3 0 5 6
// 打印指针:输出 1 2 3
printSequence(nums);
printSequence(ptr);
print("\n", .{});
}
fn printSequence(my_seq: anytype) void {
const my_typeinfo = @typeInfo(@TypeOf(my_seq));
switch (my_typeinfo) {
.array => {
print("Array:", .{});
// 数组遍历依赖的是长度 (len)
// 尽管中间有 0,循环依然会走完所有 6 个元素
for (my_seq) |s| {
print(" {}", .{s}); // 添加空格以便阅读
}
},
.pointer => {
// 获取哨兵值 (对于 [*:0]u32 来说是 0)
const my_sentinel = sentinel(@TypeOf(my_seq));
print("Many-item pointer:", .{});
var i: usize = 0;
// 指针遍历依赖的是内容
// 一旦遇到哨兵值,循环立即终止
while (my_seq[i] != my_sentinel) {
print(" {}", .{my_seq[i]});
i += 1;
}
},
else => unreachable,
}
print(". ", .{});
}
核心知识点总结
1. 数组的“上帝视角”
定长数组 [N:S]T 在编译期就知道自己有多长。
当你使用 for 循环遍历它时,编译器使用的是索引 0..N。它并不在乎内容是什么,所以它会跨过中间的哨兵值,直到打印完所有分配的元素。
2. 指针的“盲人摸象”
哨兵指针 [*:S]T 丢失了长度信息。
它操作起来就像 C 语言的 while (*str != '\0')。它必须逐个检查元素的值。一旦遇到与哨兵相等的值(本例中为 0),它就认为数据结束了。
3. 类型反射的应用
代码中的 const my_sentinel = sentinel(@TypeOf(my_seq)); 展示了 Zig 标准库的反射能力。我们不需要硬编码 0,而是让编译器去查询类型的定义:“嘿,这个指针的终止符是什么?”。这让函数变得通用。
后续预告:我们已经学习了哨兵。接下来的练习将进入标准库最常用的部分——内存分配器 (Allocators)。在 Zig 中,没有隐式的 malloc 或 new,你必须显式地管理内存。下一篇我们将学习 std.heap.page_allocator。