Ziglings 笔记 44: 阶段性测验 5 - 大象与其尾巴

牵手的大象

在 Ziglings 的第 44 个练习(Quiz 5)中,我们迎来了一个有趣的数据结构挑战:循环链表

题目引用了一首儿歌,描述大象们通过尾巴和鼻子连在一起。在编程中,这正是链表 (Linked List) 的形象比喻。

挑战:构建循环链表

我们需要:

  1. 创建三个 Elephant 结构体实例(A, B, C)。
  2. 使用指针将它们连接起来:A 的尾巴指向 B,B 指向 C,C 指回 A。
  3. 编写一个安全的遍历算法,确保不会陷入死循环。

解决方案

这是完整的实现代码:

const std = @import("std");

// 定义链表节点
const Elephant = struct {
    letter: u8,
    // 自引用指针:指向另一个 Elephant
    // 初始化为 undefined,因为在创建时我们还不知道下一个大象在哪里
    tail: *Elephant = undefined,
    visited: bool = false,
};

pub fn main() void {
    // 1. 实例化节点
    var elephantA = Elephant{ .letter = 'A' };
    var elephantB = Elephant{ .letter = 'B' }; // 补充 Elephant B
    var elephantC = Elephant{ .letter = 'C' };

    // 2. 建立链接 (A -> B -> C -> A)
    // 使用取地址符 & 获取指针
    elephantA.tail = &elephantB;
    elephantB.tail = &elephantC; // 链接 B 到 C
    elephantC.tail = &elephantA; // 闭环:C 回到 A

    visitElephants(&elephantA);

    std.debug.print("\n", .{});
}

// 遍历函数
fn visitElephants(first_elephant: *Elephant) void {
    var e = first_elephant;

    // 3. 使用 visited 标记防止死循环
    // 这里的 e 是指针,但 e.visited 自动解引用,这是 Zig 的语法糖
    while (!e.visited) {
        std.debug.print("Elephant {u}. ", .{e.letter});
        e.visited = true;
        // 移动到下一个节点
        e = e.tail;
    }
}

核心知识点回顾

1. 指针作为“链接”

这是所有动态数据结构(链表、树、图)的基石。通过在结构体中存储 *SelfType,我们可以在内存中建立非连续数据的逻辑连接。

2. 自动解引用 (Auto-dereference)

visitElephants 函数中,e 是一个指针。 但是在访问属性时,我们直接使用了 e.visitede.tail,而不是 C 语言风格的 e->tail(*e).tail。Zig 编译器帮我们处理了指针解引用的繁琐工作,让代码看起来像是在操作普通对象。

3. 算法逻辑

对于循环结构,必须有状态记录(State)来避免无限循环。这里我们简单地修改了结构体内部的 visited 字段。在更复杂的场景(或不可变数据)中,我们可能需要外部的哈希表来记录已访问的节点地址。


后续预告:我们已经彻底搞定了基础的指针和结构体。接下来,我们将进入 Zig 最具特色的功能之一——Optional 类型(?)。它是如何彻底解决空指针异常(Null Pointer Exception)的?下一篇见分晓。