
Zig 学习笔记(v0.16 入门篇)
适用环境:Windows 11 + Zig 0.16
笔记风格:零基础友好,通俗解释 + 精确概念 + 避坑指南
目录
- 什么是 Zig
- 环境安装
- Hello World
- 变量与常量
- 标识符命名规则
- 解构赋值
- 块(Block)与作用域
- 变量遮蔽(Shadowing)
- 容器(Container)与块的区别
- 注释
- 线程局部存储(threadlocal)
- 延迟执行(defer 与 errdefer)
- 下一步学什么
一、什么是 Zig
一句话概括:Zig 是一门系统级编程语言,目标是用更简单的方式写出性能高、可靠性强的软件。
- 没有隐藏控制流:没有宏、没有异常、没有函数重载,代码所见即所得
- 编译期计算能力强大:可以在编译时执行代码,生成高效的程序
- 与 C 语言无缝互操作:可以直接包含 C 头文件、调用 C 函数
- 没有垃圾回收:手动管理内存,但提供了更安全的机制
适合做什么:系统工具、嵌入式开发、游戏引擎、性能敏感的应用、替代 C/C++ 的项目。
二、环境安装
使用 Scoop 安装(推荐)
| |
验证安装
| |
我的输出示例(你的版本号可能不同):
0.16.0
常用 Zig 命令速查
| 命令 | 用途 |
|---|---|
zig build | 构建项目(配合 build.zig) |
zig build-obj | 编译成目标文件(.obj) |
zig build test | 运行测试 |
zig init | 初始化一个项目模板 |
zig fmt | 自动格式化代码 |
zig cc | 充当 C 编译器 |
zig c++ | 充当 C++ 编译器 |
zig fetch | 下载依赖包 |
zig zen | 打印 Zig 的“禅意”宣言(好玩的彩蛋) |
💡 小技巧:运行
zig --help可以查看所有命令。
三、Hello World
| |
逐行拆解:
| 代码 | 通俗解释 |
|---|---|
const std = @import("std"); | 导入 Zig 标准库,起个名字叫 std(就像 Python 的 import) |
pub fn main() !void | 定义程序的入口函数,!void 表示“可能出错,但不返回具体值” |
std.debug.print(...) | 在控制台打印文本 |
"Hello, World!\n" | 要打印的字符串,\n 是换行符 |
.{ } | 空元组,表示“没有额外的参数要填充” |
运行方式:
| |
四、变量与常量
4.1 核心概念
把变量想象成贴了标签的盒子:
- 盒子放在内存里,标签是变量名
- 盒子里可以放数据
- 有的盒子允许换内容(
var),有的不允许(const)
4.2 可变变量(var)
变量(Variable):具有内存地址的存储单元,可读且可写。
语法:
| |
示例:
| |
4.3 不可变常量(const)
常量(Constant):在 Zig 中不一定是内存位置,更像是“值的别名”,编译器会尽可能在编译时直接算出结果。
语法:
| |
示例:
| |
4.4 未初始化的变量(undefined)
有时候你需要先声明变量、稍后再赋值,可以用 undefined 占位:
| |
⚠️ 严重警告:读取未初始化的变量是未定义行为(UB)!
🔴 特别警示:undefined 对 bool 和指针的危险
| |
💀 避坑铁律:
bool和指针类型的变量,绝对不要在赋值前读取。undefined只应作为“占位符”——但在真正使用前,必须赋予有效值。
正确做法:
| |
4.5 类型推断
⚠️ 重要澄清:
| |
| 写法 | 实际类型 | 说明 |
|---|---|---|
var x = 10; | i32 | 赋值给 var 时,comptime_int 被降级为默认整数 i32 |
const y = 10; | comptime_int | 无固定位宽的编译期整数,可参与任意精度运算 |
const z: u8 = 10; | u8 | 显式类型注解决定了最终类型 |
📌 核心认知:
comptime_int只有在const且未显式指定类型时才“存活”。一旦赋给var或参与运行时运算,就会具体化为固定位宽类型。
🔧 调试技巧:查看变量的实际类型
| |
⚠️ 注意:
@compileLog会让编译失败并打印信息,仅用于调试!调试完后要删除或注释掉。
五、标识符命名规则
一句话:给变量、函数、类型起名字的规则。
5.1 基础硬性规则(必须遵守)
- 开头:字母(
a-z,A-Z)或下划线(_) - 后续:字母、数字、下划线
- 不能与关键字同名(如
var、const、if、while等)
| |
5.2 Zig 的“逃生舱”:@"" 转义(特殊情况)
如果你必须使用关键字(比如调用 C 库里的 if 函数,或 JSON 字段名叫 type),Zig 提供了 @"" 语法强制转义:
| |
📌 注意:使用转义标识符时,调用处也要带
@""。
5.3 命名风格惯例(区分硬性 vs 软性)
| 类别 | 强制还是惯例 | 要求 |
|---|---|---|
| 类型(struct/enum/union) | 强制 | 必须以大写字母开头(PascalCase),否则编译报错 |
| 变量(var/const) | 建议 | snake_case(全小写+下划线),编译器不检查 |
| 函数(fn) | 建议 | snake_case,编译器不检查 |
| |
六、解构赋值
6.1 这是什么?
一句话:把一个元组、数组或向量“拆开”,分别赋值给多个变量。
就像拆快递包裹:一个包裹里有三样东西(位置1、2、3),你可以把它们分别拿出来放到三个盒子里。
6.2 支持的数据类型
| 类型 | 写法 | 示例 |
|---|---|---|
| 元组(Tuple) | .{a, b, c} | const t = .{1, 2, 3}; |
| 数组(Array) | [_]T{a, b, c} | const arr = [_]u32{4, 5, 6}; |
| 向量(Vector) | @Vector(N, T) | const v: @Vector(3, u32) = .{7, 8, 9}; |
6.3 基本用法
| |
6.4 左侧可以怎么写?
左侧的列表由逗号分隔,每个位置支持以下写法:
| 写法 | 含义 |
|---|---|
x | 使用已存在的变量 x(必须提前声明) |
var a | 声明一个新的可变变量 a |
const b | 声明一个新的不可变变量 b |
_ | 丢弃这个位置的值(不要了) |
混合示例:
| |
6.5 ⚠️ 重要限制
解构赋值只能在块(
{ })内使用,不能在容器顶层(如 struct 内部)使用!
| |
6.6 进阶:容器内的 comptime 块
虽然容器不能包含运行时语句,但可以包含 comptime 块——因为它在编译期执行,本质上是一种“声明期的计算工具”:
| |
💡 理解要点:
comptime块就像“在编译时运行的函数”,它不产生运行时指令,只做类型检查、代码生成或编译期校验——所以容器允许它存在。
6.7 编译期解构
加 comptime 前缀,让它在编译时执行:
| |
一句话记忆:解构就是“拆包”——把右边的元组/数组/向量拆开,分别放进左边的变量里。
七、块(Block)与作用域
7.1 什么是块?
一句话:
{ }花括号包裹的代码区域就是块。块用来限定变量的可见范围。
7.2 作用域规则
块内声明的变量,块外不能访问:
| |
生活类比:就像房间里摆的东西,出了房间门就看不见也摸不着了。
7.3 块也是表达式(可以返回值)
块可以带一个标签(名字),然后用 break :标签 值 从块中返回数据:
| |
标签名可以随意起:result:、compute:、init: 都可以。
7.4 空块
{ } 空块等效于 void{},表示“什么都没有”。
一句话记忆:块 =
{}包围的代码区域,限制变量作用域,还能当表达式返回值。
八、变量遮蔽(Shadowing)
8.1 什么是变量遮蔽?
在内部作用域中,声明一个与外部作用域同名的变量,导致外部变量被“挡住”了——这就叫遮蔽。
其他语言(比如 JavaScript)允许:
| |
8.2 Zig 的态度:坚决禁止!
| |
8.3 为什么禁止?
- 防止写错变量:不小心操作了错误的同名变量,这种 Bug 很难追踪
- 代码更容易读懂:变量名在作用域内是唯一的,不迷路
- 强制清晰命名:逼你给不同用途的变量取不同的名字
8.4 唯一的例外
两个完全不重叠的作用域,可以使用同名变量:
| |
一句话记忆:Zig 禁止变量遮蔽——同一作用域内,名字不能重复用。
九、容器(Container)与块的区别
这是初学者最容易混淆的概念,务必仔细看!
9.1 一句话区分
| 块(Block) | 容器(Container) | |
|---|---|---|
| 是什么 | 执行代码的包裹体 | 存放定义的“收纳盒” |
| 放什么 | 放动作(语句) | 放东西(声明) |
| 能不能放 if/for | ✅ 能 | ❌ 不能 |
9.2 什么是容器?
容器是 Zig 中存放声明的地方,包括:
struct(结构体)enum(枚举)union(联合)opaque(不透明类型)- 整个
.zig源文件
容器只能放声明(变量声明、常量声明、函数声明),不能放语句(if、for、while、赋值操作等)。
9.3 对比示例
| |
9.4 📌 重要推论
Zig 源文件的顶层也是容器,所以:
- ✅ 文件顶层可以放:
const、var、fn、struct、enum、union、import - ❌ 文件顶层不能放:
if、for、while、return、赋值语句
| |
一句话记忆:
- 容器 = 放“有什么”(声明列表)
- 块 = 放“做什么”(执行指令)
- 源文件顶层是容器,函数体内部是块
十、注释
10.1 三种注释
| 符号 | 名称 | 说明 |
|---|---|---|
// | 普通注释 | 给程序员看的备注,编译器忽略 |
/// | 文档注释 | 给“紧随其后的代码”写说明,用于生成 API 文档 |
//! | 顶层文档注释 | 给“整个文件”写说明,放在文件开头 |
💡 实用提示:这些注释不仅是给人看的,Zig Language Server(zls)会在编辑器悬停提示时直接显示
///和//!的内容,是写库必备的技能。
10.2 示例
| |
10.3 ⚠️ 重要限制
//! 必须放在作用域的顶层,否则编译错误:
| |
一句话记忆:
//→ 程序员之间的悄悄话///→ 给这个“东西”写说明书//!→ 给这个“文件”写说明书
十一、线程局部存储(threadlocal)
11.1 这是什么?
一句话:每个线程都有一份独立的变量副本,互不干扰。
11.2 为什么需要它?
想象一个场景:你开了三个窗口(线程)同时在网站上买东西。每个窗口都有自己的“购物车”,互不影响——这就是线程局部存储。
11.3 用法
在 var 前面加 threadlocal:
| |
输出示例(顺序可能不同):
x = 1234 (主线程)
x = 1235 (主线程)
x = 1234 (线程1)
x = 1235 (线程1)
x = 1234 (线程2)
x = 1235 (线程2)
11.4 ⚠️ 限制
- 初始化值必须是编译期常量:
| |
- 单线程模式不支持:如果构建目标为 WebAssembly 或某些嵌入式系统(单线程),
threadlocal不可用,编译时会报错。
11.5 使用场景
- 线程 ID
- 数据库连接池的线程局部句柄
- 线程私有的缓存或缓冲区
- 不需要锁的线程安全计数器
一句话记忆:
threadlocal var x让每个线程都有自己的x,改自己的不影响别人。
十二、延迟执行(defer 与 errdefer)
12.1 什么是 defer?
一句话:
defer后面的代码,会在当前作用域结束前执行——无论是正常退出、return,还是发生错误。
生活类比:就像下班离开办公室前,一定会锁门、关灯。不管你今天工作顺不顺利,这些善后动作都要做。
| |
12.2 defer 的执行顺序
后进先出(LIFO):像栈一样,后定义的 defer 先执行。
| |
12.3 errdefer——只处理错误场景
一句话:只有当作用域因错误退出时,
errdefer才会执行。正常退出时它不执行。
| |
12.4 对比速查
| 关键字 | 触发时机 | 典型用途 |
|---|---|---|
defer | 作用域结束时(无论是否出错) | 释放分配器、解锁互斥锁、关闭文件 |
errdefer | 作用域因错误退出时 | 出错时关闭文件、回滚事务 |
一句话记忆:
defer保命,errdefer保错误现场。
十三、下一步学什么
掌握以上内容后,建议继续学习:
- 基本类型:整数、浮点数、布尔值、可选类型(
?T)、错误联合(!T) - 控制流:
if、while、for、switch - 函数:参数传递、返回值、
comptime参数 - 内存管理:分配器(Allocator)、切片(Slice)、数组
- 结构体与方法:
struct的字段、方法、默认值 - 错误处理:
try、catch、errdefer(已在第十二节中初步接触) - 编译期编程:
comptime、@内置函数、泛型
📌 提前告知:Zig 的“泛型”不像 C++ 的模板或 Java 的泛型。它是通过
anytype+comptime参数实现的——本质上是在编译期“把类型当作参数传递”。等你学到comptime时,会发现它异常强大。
📌 总复习:核心要点速查
| 知识点 | 一句话总结 |
|---|---|
| var | 声明可变变量,可以修改 |
| const | 声明不可变变量,不能修改 |
| undefined | 占位符,使用前必须赋值;bool/指针尤其危险 |
| 类型推断 | var x = 10 → i32;const y = 10 → comptime_int |
| @compileLog(@TypeOf(x)) | 编译期打印变量类型(调试工具) |
| 标识符规则 | 字母/下划线开头,不能是关键字;@"" 可以转义 |
| 类型命名 | 必须大写开头(PascalCase),否则报错 |
| 变量/函数命名 | 建议 snake_case,但编译器不强制 |
| 解构赋值 | x, var y, const z = tuple; 拆包赋值 |
| 解构位置限制 | 只能在块({})内使用 |
| 块(Block) | {} 包裹的作用域,可以返回值 |
| 变量遮蔽 | Zig 禁止遮蔽同名变量 |
| 容器(Container) | struct/enum/源文件顶层,只能放声明,不能放语句 |
| 容器内 comptime 块 | ✅ 允许,因为它在编译期执行 |
| 块 vs 容器 | 块放“动作”(语句),容器放“东西”(声明) |
| 注释 | // 普通,/// 文档,//! 文件文档 |
| threadlocal | 每个线程有独立副本,互不干扰;初始化必须是编译期常量 |
| defer | 作用域结束时执行(无论成败) |
| errdefer | 仅当作用域因错误退出时执行 |

