Skip to content

Day 3: 第一个Zig程序 - Hello World详解

在配置好开发环境后,我们终于可以深入代码了。今天,我们将重新审视并详细解剖第一天遇到的 “Hello, World!” 程序。通过理解这个最简单的程序,我们可以洞察Zig的执行模型、核心语法约定以及与标准库的交互方式。

让我们再次贴出 “Hello, World!” 的代码:

const std = @import("std");
pub fn main() !void {
std.debug.print("Hello, World!\n", .{});
}

这段代码虽短,但包含了Zig程序的基本骨架:

  • pub fn main() !void { ... }
    • pub: pub 是 public 的缩写,表示这个函数是公共的,可以被外部文件(在这里是Zig的编译器)调用。程序的入口点 main 函数必须是 pub。
    • fn main(): 这声明了一个名为 main 的函数。与C、Java等语言一样,main 函数是Zig程序执行的起点。
    • !void: 这是函数的返回类型。它是一个“错误联合类型”(Error Union)。
      • void 表示这个函数在成功执行时,不返回任何值。
      • ! 是一个语法糖,表示该函数除了返回 void 之外,还可能返回一个错误。这体现了Zig显式错误处理的哲学。我们将在后续章节深入探讨错误处理。
  • const std = @import("std");

    • @import("std"): 这是一个内置函数(Built-in Function),用于导入Zig的模块。在这里,我们导入了功能丰富的标准库(std)。
    • const std = ...: 这行代码将导入的标准库模块赋值给一个名为 std 的常量。按照惯例,标准库通常被命名为 std。const 关键字意味着 std 这个标识符在之后不能被重新赋值。
  • std.debug.print(...)

    • 这是对 std 模块中 debug 子模块下的 print 函数的调用。
    • std.debug.print 是一个非常有用的函数,用于向标准错误流(stderr)打印格式化的字符串。它主要用于调试目的。
    • 第一个参数 "Hello, World!\n" 是要打印的格式化字符串。
    • 第二个参数 .{} 是一个匿名的空元组(tuple),用于传递格式化参数。因为我们的字符串中没有占位符(如 {d} 或 {s}),所以这里传递一个空元组。

我们使用 zig run 命令来执行程序:

Terminal window
zig run hello.zig

这个命令实际上完成了两个步骤:

  1. 编译 (Compile):zig 编译器读取 hello.zig,进行语法分析、类型检查,并将其编译成机器码,生成一个可执行文件。
  2. 运行 (Run):执行上一步生成的可执行文件。

Zig的编译器非常智能,它会缓存编译结果。如果你再次运行 zig run hello.zig 而不修改 hello.zig 文件,编译器会发现源文件没有变化,直接运行缓存中的可执行文件,速度极快。这个缓存目录通常是项目下的 zig-cache。

main 函数签名中的 !void 到底意味着什么?这意味着 main 在执行过程中有可能失败。例如,std.debug.print 在尝试写入标准输出时,可能会遇到IO错误。如果 print 函数失败并返回一个错误,main 函数会立即将这个错误向上传播给操作系统,程序会以一个非零的退出码终止。

这种设计强制程序员去思考“代码可能失败”的情况,而不是忽略它们。

Zig提供了一个强大的编译期工具 @compileLog,它可以在编译阶段打印信息。让我们用它来观察一个值:

const std = @import("std");
pub fn main() !void {
const message = "Hello, World!\n";
@compileLog("The message is:", message);
std.debug.print(message, .{});
}

当你运行 zig run 时,你会在编译器的输出中看到 The message is: Hello, World!\n。这证明 @compileLog 是在代码执行之前,即编译阶段运行的。

修改 hello.zig 程序,使其能够接收并打印命令行参数。

提示:

  • 你需要使用 std.process.argsAlloc 来获取参数。
  • 这需要一个分配器(allocator),你可以使用 std.heap.page_allocator。
  • 记得在使用完分配的内存后,使用 defer 来释放它。

这是一个有挑战性的练习,涉及到内存分配和错误处理,预习了后续章节的内容。如果遇到困难,可以先尝试打印一个固定的字符串数组。

问:为什么我的程序编译时出现 “segmentation fault”?

答:这通常不是Zig程序本身的问题,而是Zig编译器或 zls (语言服务器) 的bug。Zig仍在快速开发中,偶尔会遇到这类问题。请尝试更新到最新的master版本或报告bug。在学习阶段,确保你的代码逻辑清晰,可以减少触发编译器bug的可能性。

问:std.debug.print 和 std.io.getStdOut().writer().print() 有什么区别?

答:std.debug.print 主要用于调试,它直接写入标准错误流,并且在Release模式下可能会被优化掉。而 std.io 中的函数提供了更通用的IO操作,用于写入标准输出流(stdout),是生产代码中处理输出的正确方式。

今天,我们深入剖析了最基础的Zig程序。我们学习了 pub fn main() !void 的结构、如何用 @import 导入标准库,以及 zig run 背后的缓存机制。我们还初步接触了错误处理和编译期编程的概念。

尽管 “Hello, World!”很简单,但它为我们理解Zig的诸多核心设计理念打开了一扇窗。明天,我们将系统地学习Zig的基础语法规则。