第6章 指针与可选类型
第6章 指针与可选类型
在开始我们的下一个大项目——从零构建 HTTP 服务器之前,我们需要补全最后一块拼图:深入理解 指针 (Pointers) 和 可选类型 (Optionals)。
Zig 的指针虽然概念上源于 C,但它引入了许多现代化的安全特性。而可选类型则是 Zig 消除“空指针异常”这一千古难题的关键武器。
1. 指针基础
简单来说,指针就是一个存储内存地址的变量。它不存储值本身,而是存储值在哪里。
创建与解引用
- 取地址 (
&):在变量前加&获取其内存地址。 - 解引用 (
.*):在指针后加.*访问该地址存储的值。
const std = @import("std");
pub fn main() !void {
const number: u8 = 5;
// pointer 的类型是 *const u8 (指向常量 u8 的指针)
const pointer = &number;
// 解引用指针,读取值
const doubled = 2 * pointer.*;
std.debug.print("Doubled: {d}\n", .{doubled}); // 输出 10
}
为什么使用指针?
主要原因是避免复制。 如果一个结构体很大(比如几 MB 的图片数据),在函数间传递它的副本会非常慢。传递指针只需要复制 8 个字节(64位系统),这被称为“引用传递”。
此外,指针允许我们修改原始数据。
var number: u8 = 5;
const pointer = &number; // 类型是 *u8
// 修改指针指向的值
pointer.* = 6;
2. 常量与变量:谁不可变?
指针涉及两层可变性,初学者容易混淆:
- 指针指向的值是否可变?
- 指针变量本身是否可变?
指向常量的指针 (*const T)
如果原变量是 const,取地址后得到的是 *const T。你不能通过这种指针修改值。
const number = 5;
const ptr = &number;
// ptr.* = 6; // 编译错误!无法修改常量
指针本身的常量性
如果指针变量是用 const 声明的,你不能让它指向别的地址。
var a: u8 = 10;
var b: u8 = 20;
// 1. const 指针:不能指向别处,但可以通过它修改 a
const ptr1: *u8 = &a;
ptr1.* = 11;
// ptr1 = &b; // 错误!ptr1 是 const
// 2. var 指针:可以指向别处
var ptr2: *u8 = &a;
ptr2 = &b; // 合法,现在指向 b
ptr2.* = 21; // 修改的是 b
3. 指针类型:单项 vs 多项
Zig 严格区分了两种指针:
单项指针 (*T)
- 指向单个对象。
- 不支持指针运算(如
ptr + 1)。 - 这是 Zig 中最常用的指针,安全且高效。
多项指针 ([*]T)
- 指向一组未知数量的元素(类似 C 语言的指针)。
- 支持指针运算。
- 不安全:编译器无法进行边界检查。
通常我们不直接创建多项指针,而是通过切片或数组隐式转换得到。
指针运算
如果你确实需要像 C 语言那样遍历内存,可以使用多项指针:
const array = [_]i32{ 1, 2, 3, 4 };
// 将数组转换为多项指针
var ptr: [*]const i32 = &array;
std.debug.print("First: {d}\n", .{ptr[0]});
// 指针前移
ptr += 1;
std.debug.print("Second: {d}\n", .{ptr[0]});
最佳实践:尽量使用切片 (
[]T) 代替指针运算。切片在底层就是指针,但它附带了长度信息,Zig 编译器能帮你检查越界错误。
4. 可选类型 (Optionals)
在 C 语言中,指针可能是 NULL。这是无数 Bug 的根源。
在 Zig 中,普通指针 (*T) 永远不为 null。
如果你需要表示“空值”或“缺失”,必须使用 可选类型,语法是在类型前加问号 ?。
什么是可选类型?
?i32 意味着:这可能是一个 i32,也可能是 null。
var number: ?i32 = 5; // 有值
number = null; // 设为空
可选指针
这一点在指针上尤为重要。
*i32:必须指向一个整数,绝不为 null。?*i32:可以是null,也可以指向一个整数。
易混淆:?*T vs *?T
?*i32(可选的指针):指针变量本身可以是null。如果它不为 null,它指向一个有效的i32。*?i32(指向可选值的指针):指针变量本身不为 null。但它指向的内存里存的是一个?i32(可能是null)。
var val: i32 = 10;
var opt_val: ?i32 = null;
var ptr1: ?*i32 = &val; // 指针本身可为 null
var ptr2: *?i32 = &opt_val; // 指针有效,指向的值是 null
5. 安全处理可选值
既然有了 null,使用前就必须检查。Zig 强制要求在使用可选值之前对其进行“解包”。
方法一:if 解包 (Payload Capture)
这是最推荐的方式。
const maybe_num: ?i32 = 42;
if (maybe_num) |num| {
// 在这里,num 是解包后的非空 i32
std.debug.print("Number is {d}\n", .{num});
} else {
std.debug.print("It was null\n", .{});
}
方法二:orelse 提供默认值
如果为 null,则使用默认值。
const maybe_score: ?u8 = null;
const score = maybe_score orelse 0; // 结果为 0
orelse 右边可以是 return 或 break,用于在空值时提前退出流程。
方法三:? 强制解包
如果你百分百确定值不为空,可以用 .? 强制提取。
警告:如果实际上是 null,程序会崩溃 (Panic)。
fn mustReturnId() ?u8 { return 1; }
pub fn main() !void {
const id = mustReturnId().?; // 得到 u8
}
总结
Zig 的内存安全哲学体现在:
- 指针默认非空:消除了隐式空指针风险。
- 指针运算受限:通过区分单项/多项指针,防止随意的内存越界。
- 显式可选类型:用
?T明确标记可能为空的数据,并强制开发者处理null情况。
掌握了这些,我们就具备了处理复杂数据结构的能力。下一章,我们将正式开始构建 HTTP 服务器,届时你将看到这些概念如何在网络编程中大显身手。