第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. 常量与变量:谁不可变?

指针涉及两层可变性,初学者容易混淆:

  1. 指针指向的值是否可变?
  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 右边可以是 returnbreak,用于在空值时提前退出流程。

方法三:? 强制解包

如果你百分百确定值不为空,可以用 .? 强制提取。 警告:如果实际上是 null,程序会崩溃 (Panic)。

fn mustReturnId() ?u8 { return 1; }

pub fn main() !void {
    const id = mustReturnId().?; // 得到 u8
}

总结

Zig 的内存安全哲学体现在:

  1. 指针默认非空:消除了隐式空指针风险。
  2. 指针运算受限:通过区分单项/多项指针,防止随意的内存越界。
  3. 显式可选类型:用 ?T 明确标记可能为空的数据,并强制开发者处理 null 情况。

掌握了这些,我们就具备了处理复杂数据结构的能力。下一章,我们将正式开始构建 HTTP 服务器,届时你将看到这些概念如何在网络编程中大显身手。


原文出处: https://pedropark99.github.io/zig-book/