承接前文

在上一篇文章中,我们深入了解了 Zig 的基础知识:如何声明变量、理解数据类型,以及掌握代码块和作用域的概念。我们了解到 Zig 是一门强调显式优于隐式的语言,编译器的严格检查帮助我们在编译期就发现潜在问题。

今天,我们将继续 Zig 的学习之旅,探索三个核心主题:

  1. 数组(Arrays) - 固定大小的同类型数据集合
  2. 字符串(Strings) - Zig 中特殊的字节序列
  3. 内存管理 - 使用 defererrdefer 确保内存安全

这些概念是理解 Zig 内存模型的关键,也是编写健壮 Zig 程序的基础。

带标签的代码块

在深入数组之前,我们先来了解一下 Zig 中代码块的一个强大特性:带标签的代码块(Labeled Blocks)

回顾上一篇的内容,我们知道代码块可以作为表达式使用。而当我们给代码块添加标签后,可以使用 break 关键字从代码块中返回值,就像从函数返回一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const std = @import("std");

pub fn main() void {
var y: i32 = 123;

// 带标签的代码块
const x = add_one: {
y += 1;
break :add_one y; // 从代码块返回 y 的值
};

std.debug.print("x = {d}, y = {d}\n", .{x, y});
// 输出: x = 124, y = 124
}

带标签的代码块在需要根据复杂条件计算初始值时非常有用,它让代码逻辑更加清晰。

数组与切片

数组的回顾与深入

在上一篇中,我们初步接触了数组。数组是 Zig 中最基础的数据结构之一,它的核心特点是:大小必须在编译时确定

1
2
3
4
5
6
7
8
// 基本数组声明
const nums = [4]u8{48, 24, 12, 6};

// 使用 _ 让编译器自动推断大小
const values = [_]f64{432.1, 87.2, 900.05};

// 使用 ** 运算符创建重复值的数组
const zeros = [_]u8{0} ** 10; // 10 个 0

数组的类型格式为 [N]T,其中 N 是元素个数,T 是元素类型。这种设计让编译器能够在编译期进行严格的边界检查。

切片(Slice)

切片是 Zig 中另一个重要的概念。与数组不同,切片的大小在运行时确定。切片本质上是一个"胖指针",包含两个部分:

  • 指向数据的指针 [*]T
  • 元素数量 usize
1
2
3
4
5
6
7
const nums = [4]u8{48, 24, 12, 6};

// 从数组创建切片
const slice = nums[1..3]; // 包含索引 1 和 2 的元素

// 切片的类型是 []u8(或 []const u8)
std.debug.print("切片类型: {s}\n", .{@typeName(@TypeOf(slice))});

切片与数组的对比:

特性 数组 切片
大小确定时机 编译时 运行时
类型表示 [N]T []T[]const T
内存占用 仅数据本身 指针 + 长度 + 数据
使用场景 固定大小数据 动态大小数据

字符串

字符串的本质

在 Zig 中,字符串本质上就是字节序列。Zig 没有专门的 string 类型,而是使用 u8 的数组或切片来表示字符串。

1
2
3
4
5
6
// 字符串字面量
const literal = "Hello, Zig!";

// 查看类型
std.debug.print("类型: {s}\n", .{@typeName(@TypeOf(literal))});
// 输出: *const [11:0]u8

注意输出中的 [11:0]

  • 11 表示字符串长度(11 个字符)
  • :0 表示这是一个**哨兵终止(sentinel-terminated)**数组,以 0 结尾

哨兵终止数组 vs 切片

Zig 中有两种主要的字符串表示方式:

1. 哨兵终止数组(Sentinel-terminated Array)

这种数组在末尾有一个特殊的"哨兵值"(通常是 0),类似于 C 语言中的空终止字符串:

1
2
3
4
5
6
// 字符串字面量默认是哨兵终止的
const str = "Hello";
// 类型: *const [5:0]u8

// 显式创建哨兵终止数组
const arr: [5:0]u8 = .{'H', 'e', 'l', 'l', 'o'};

2. 切片(Slice)

标准库函数通常接收字符串切片作为参数:

1
2
3
4
5
const str: []const u8 = "Hello, World!";
// 类型: []const u8

// 切片不要求以 0 结尾
const slice: []const u8 = &[_]u8{'H', 'i'};

字符串常用操作

Zig 标准库的 std.mem 模块提供了丰富的字符串操作函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const std = @import("std");

pub fn main() void {
const str1 = "Hello";
const str2 = "World";

// 字符串比较
const equal = std.mem.eql(u8, str1, str2);

// 检查前缀/后缀
const starts_with = std.mem.startsWith(u8, str1, "He");
const ends_with = std.mem.endsWith(u8, str1, "lo");

// 分割字符串
var iter = std.mem.splitScalar(u8, "a,b,c", ',');
while (iter.next()) |part| {
std.debug.print("{s}\n", .{part});
}

// 去除空白
const trimmed = std.mem.trim(u8, " hello ", " ");

// 统计子串出现次数
const count = std.mem.count(u8, "abababa", "aba");

std.debug.print("equal: {}, starts_with: {}, ends_with: {}, count: {d}\n",
.{equal, starts_with, ends_with, count});
}

常用字符串函数列表:

函数 功能
std.mem.eql() 比较两个字符串是否相等
std.mem.splitScalar() 按单个字符分割字符串
std.mem.splitSequence() 按子串分割字符串
std.mem.startsWith() 检查字符串是否以指定前缀开头
std.mem.endsWith() 检查字符串是否以指定后缀结尾
std.mem.trim() 去除字符串首尾的指定字符
std.mem.concat() 连接多个字符串
std.mem.count() 统计子串出现次数
std.mem.replace() 替换子串

Unicode 处理

如果需要处理 Unicode 字符(如中文、日文等),可以使用 std.unicode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const std = @import("std");

pub fn main() !void {
// 日文 "アメリカ"(美国)
const text = "アメリカ";

// 使用 Utf8View 创建迭代器
var utf8 = try std.unicode.Utf8View.init(text);
var iter = utf8.iterator();

while (iter.nextCodepointSlice()) |codepoint| {
std.debug.print("码点: {x}\n", .{codepoint});
}
// 输出每个字符的 UTF-8 编码
}

注意:Zig 的字符串是按字节索引的,处理多字节字符时需要使用 Unicode 迭代器。

内存管理与 defer

内存安全的重要性

在系统编程中,内存管理是最容易出现问题的领域。常见的内存错误包括:

  • 内存泄漏:分配了内存但未释放
  • 使用已释放内存:访问已经 free 的内存
  • 双重释放:对同一块内存调用两次 free
  • 缓冲区溢出:访问数组边界外的内存

Rust 通过所有权和借用检查器强制内存安全,而 Zig 采取了一种不同的哲学:提供工具,但不强制。Zig 相信程序员应该有能力写出安全的代码,而不是被编译器限制。

defer:延迟执行

defer 是 Zig 中最重要的内存管理工具之一。它确保在当前作用域结束时执行指定的清理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const std = @import("std");

pub fn main() !void {
const allocator = std.heap.page_allocator;

// 分配内存
const ptr = try allocator.alloc(u8, 100);

// 确保内存会被释放
defer allocator.free(ptr);

// 使用内存...
ptr[0] = 42;

// 函数结束时,defer 语句自动执行 free
}

defer 的核心优势:

  1. 物理位置接近:分配和释放代码写在一起,便于维护
  2. 逻辑绑定:释放操作与作用域生命周期绑定
  3. 避免遗忘:无论函数如何返回,清理代码都会执行

errdefer:错误时执行

errdeferdefer 的变体,它只在函数返回错误时执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const std = @import("std");

fn doSomething() !void {
const allocator = std.heap.page_allocator;

const resource1 = try allocateResource1();
defer deallocateResource1(resource1);

const resource2 = try allocateResource2();
// 如果 allocateResource2 失败,resource1 会被 defer 释放
// 但如果 resource2 成功,之后发生错误,我们需要释放 resource2
errdefer deallocateResource2(resource2); // 只在错误时执行

// 进行一些可能失败的操作
try mightFail();

// 成功后,errdefer 不会执行
// 但我们需要确保 resource2 最终被释放
defer deallocateResource2(resource2);
}

errdefer 的典型使用场景是资源的部分初始化:当函数在初始化多个资源时失败,需要清理已经分配的资源。

defer 的执行顺序

多个 defer 语句按照**后进先出(LIFO)**的顺序执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const std = @import("std");

pub fn main() void {
defer std.debug.print("1\n", .{});
defer std.debug.print("2\n", .{});
defer std.debug.print("3\n", .{});

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

// 输出:
// 执行中...
// 3
// 2
// 1

这个特性与栈的行为一致,确保资源按照正确的顺序释放(先分配的后释放)。

Zig 的内存安全特性总结

特性 作用
defer 作用域结束时执行清理
errdefer 发生错误时执行清理
非空指针 指针默认不可为空,避免空指针解引用
测试分配器 检测内存泄漏和双重释放
数组边界检查 编译期和运行时的边界检查

实战练习

让我们通过几个 Ziglings 练习来巩固所学知识,包括从 004 - 008 的练习。

练习:数组操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const std = @import("std");

pub fn main() void {
// 创建一个数组
const arr = [_]u8{1, 2, 3, 4, 5};

// 使用切片获取子数组
const slice = arr[1..4];
std.debug.print("切片: {any}\n", .{slice});

// 使用 ** 运算符
const repeated = [_]u8{1, 2} ** 3;
std.debug.print("重复: {any}\n", .{repeated});

// 使用 ++ 运算符
const combined = [_]u8{1, 2} ++ [_]u8{3, 4};
std.debug.print("连接: {any}\n", .{combined});
}

练习:字符串处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const std = @import("std");

pub fn main() void {
const text = "Hello, Zig World!";

// 检查前缀
if (std.mem.startsWith(u8, text, "Hello")) {
std.debug.print("以 Hello 开头\n", .{});
}

// 分割字符串
var iter = std.mem.splitScalar(u8, "apple,banana,cherry", ',');
while (iter.next()) |fruit| {
std.debug.print("水果: {s}\n", .{fruit});
}
}

练习:使用 defer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const std = @import("std");

fn readFile(path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close(); // 确保文件被关闭

const size = try file.getEndPos();
const allocator = std.heap.page_allocator;

const buffer = try allocator.alloc(u8, size);
errdefer allocator.free(buffer); // 如果读取失败,释放内存

_ = try file.readAll(buffer);

return buffer;
}

总结

本文我们深入学习了 Zig 的三个核心概念:

  1. 带标签的代码块:让代码块可以返回值,简化条件初始化逻辑
  2. 数组与字符串
    • 数组大小编译时确定,类型为 [N]T
    • 切片大小运行时确定,类型为 []T
    • 字符串是 u8 的数组或切片
    • 标准库提供丰富的字符串操作函数
  3. 内存管理
    • defer 确保资源在作用域结束时释放
    • errdefer 处理错误时的资源清理
    • Zig 提供工具而非强制,让程序员掌控内存安全

Zig 的设计理念是显式优于隐式,通过 defer 等机制,我们可以在保持代码清晰的同时,确保内存安全。这种设计让 Zig 既拥有 C 语言的灵活性,又能避免许多常见的内存错误。

在下一篇文章中,我们将探索 Zig 的控制流(if、switch、while、for)和函数,继续我们的 Zig 学习之旅!