承接前文
在上一篇文章中,我们深入了解了 Zig 的基础知识:如何声明变量、理解数据类型,以及掌握代码块和作用域的概念。我们了解到 Zig 是一门强调显式优于隐式的语言,编译器的严格检查帮助我们在编译期就发现潜在问题。
今天,我们将继续 Zig 的学习之旅,探索三个核心主题:
- 数组(Arrays) - 固定大小的同类型数据集合
- 字符串(Strings) - Zig 中特殊的字节序列
- 内存管理 - 使用
defer 和 errdefer 确保内存安全
这些概念是理解 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 中另一个重要的概念。与数组不同,切片的大小在运行时确定。切片本质上是一个"胖指针",包含两个部分:
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 的核心优势:
- 物理位置接近:分配和释放代码写在一起,便于维护
- 逻辑绑定:释放操作与作用域生命周期绑定
- 避免遗忘:无论函数如何返回,清理代码都会执行
errdefer:错误时执行
errdefer 是 defer 的变体,它只在函数返回错误时执行:
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 的三个核心概念:
- 带标签的代码块:让代码块可以返回值,简化条件初始化逻辑
- 数组与字符串:
- 数组大小编译时确定,类型为
[N]T
- 切片大小运行时确定,类型为
[]T
- 字符串是
u8 的数组或切片
- 标准库提供丰富的字符串操作函数
- 内存管理:
defer 确保资源在作用域结束时释放
errdefer 处理错误时的资源清理
- Zig 提供工具而非强制,让程序员掌控内存安全
Zig 的设计理念是显式优于隐式,通过 defer 等机制,我们可以在保持代码清晰的同时,确保内存安全。这种设计让 Zig 既拥有 C 语言的灵活性,又能避免许多常见的内存错误。
在下一篇文章中,我们将探索 Zig 的控制流(if、switch、while、for)和函数,继续我们的 Zig 学习之旅!