Skip to content
zig 版本:0.13.0

指针

zig 作为一门 low level 语言,那肯定要有指针的。

指针是指向一块内存区域地址的变量,它存储了一个地址,我们可以通过指针来操作其指向内存区域。

取地址:通过 & 符号来获取某个变量所对应的内存地址,如 &integer 就是获取变量 integer 的内存地址。

zig 的指针和 C 的指针略有不同,包含两种指针,一种单项(single-item)指针,一种是多项(many-item)指针,它们的解引用的方式也略有不同。

关于指针运算

zig 本身支持指针运算(加减操作),但有一点需要注意:最好将指针分配给 [*]T 类型后再进行计算。

尤其是在切片中,不可直接对其指针进行更改,这会破坏切片的内部结构!

单项指针

单项指针指向单个元素。

单项指针的类型为:*TT是所指向内存区域的类型,解引用方法是 ptr.*

zig
const print = @import("std").debug.print;

pub fn main() !void {
    var integer: i16 = 666;
    const ptr = &integer;
    ptr.* = ptr.* + 1;

    print("{}\n", .{integer});
}

🅿️ 提示

函数指针略有特殊:const Call2Op = *const fn (a: i8, b: i8) i8;

多项指针

多项指针指向位置数量的多个元素。

多项指针的类型为:[*]TT是所指向内存区域的类型,且该类型必须具有明确的大小(这意味着它不能是 anyopaque 和其他任意不透明类型)。

解引用方法支持以下几种:

  • 索引语法 ptr[i]
  • 切片语法 ptr[start..end]
  • 指针运算 ptr + xptr - x
zig
const print = @import("std").debug.print;

pub fn main() !void {
    const array = [_]i32{ 1, 2, 3, 4 };
    const ptr: [*]const i32 = &array;

    print("第一个元素:{}\n", .{ptr[0]});
}

🅿️ 提示

数组和切片都与指针有紧密的联系。

*[N]T:这是指向一个数组的单项指针,数组的长度为N。也可以将其理解为指向N个元素的指针。

[]T:这是切片,相当于一个胖指针,包含了一个类型为 [*]T 的指针和一个长度。

数组指针的类型中就包含了长度信息,而切片中则实际存储着长度。数组指针和切片的长度都可以通过 len 属性来获取。

示例
zig
const expect = @import("std").testing.expect;

pub fn main() !void {
    var array: [5]u8 = "hello".*;

    const array_pointer = &array;
    try expect(array_pointer.len == 5);

    const slice: []u8 = array[1..3];
    try expect(slice.len == 2);
}

哨兵指针

哨兵指针就和哨兵数组类似,我们使用语法 [*:x]T,这个指针标记了边界的值,故称为“哨兵”。

它的长度有标记值 x 来确定,这样做的好处就是提供了针对缓冲区溢出和过度读取的保护。

示例

我们接下来演示一个示例,该示例中使用了 zig 可以无缝与 C 交互的特性,故你可以暂时略过这里!

zig
const std = @import("std");

// 我们也可以用 std.c.printf 代替
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;

pub fn main() anyerror!void {
    _ = printf("Hello, world!\n"); // OK
}

以上代码编译需要额外连接 libc ,你只需要在你的 build.zig 中添加 exe.linkLibC(); 即可。

多项指针和单向指针区别

本部分专门用于解释并区别单向指针和多项指针!

先列出以下类型:

  • [4] const u8

该类型代表的是一个长度为 4 的数组,数组内的元素类型为 const u8

  • [] const u8

该类型代表的是一个切片,切片内元素类型为 const u8

  • *[4] const u8

该类型代表的是一个指针,它指向一个内存地址,内存中该地址存储着一个长度为 4 的数组,数组内的元素类型为 const u8

  • *[] const u8

该类型代表的是一个指针,它指向一个内存地址,内存中该地址存储着一个切片。

  • [*] const u8

该类型代表的是一个指针,它指向一个内存地址,内存中该地址存储着一个数组,但长度未知!!

其中 [*] const u8 可以看作是 C 中的 * const char,这是因为在 C 语言中一个普通的指针也可以指向一个数组,zig 仅仅是单独把这种令人迷惑的行为单独作为一个语法而已!

额外特性

以下的是指针的额外特性,初学者可以直接略过以下部分,等到你需要时再来学习即可!

volatile

如果不知道什么是指针操作的“ 副作用 ”,那么这里你可以略过,等你需要时再来查看!

对指针的操作应假定为没有副作用。如果存在副作用,例如使用内存映射输入输出(Memory Mapped Input/Output),则需要使用 volatile 关键字来修饰。

在以下代码中,保证使用 mmio_ptr 的值进行操作(这里你看起来可能会感到迷惑,在编译代码时,编译器可以能会让值在实际运行过程中进行缓存,这里保证每次都使用 mmio_ptr 的值,以避免无法正确触发 “副作用”),并保证了代码执行的顺序。

zig
// expect 是单元测试的断言函数
const expect = @import("std").testing.expect;

pub fn main() !void {
    const mmio_ptr: *volatile u8 = @ptrFromInt(0x12345678);
    try expect(@TypeOf(mmio_ptr) == *volatile u8);
}

该节内容,仅仅讲述的少量内容,如果要了解更多,你可能需要查看官方文档

对齐

如果你不知道内存对齐的含义是什么,那么本节内容你可以跳过了,等到你需要时再来查看!

每种类型都有一个对齐方式——数个字节,这样,当从内存加载或存储该类型的值时,内存地址必须能被该数字整除。我们可以使用 @alignOf 找出任何类型的内存对齐大小。

内存对齐大小取决于 CPU 架构,但始终是 2 的幂,并且小于 1 << 29。

在 Zig 中,指针类型具有对齐值。如果该值等于基础类型的对齐方式,则可以从类型中省略它:

zig
const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;

pub fn main() !void {
    var x: i32 = 1234;
    // 获取内存对齐信息
    const align_of_i32 = @alignOf(@TypeOf(x));
    // 尝试比较类型
    try expect(@TypeOf(&x) == *i32);
    // 尝试在设置内存对齐后再进行类型比较
    try expect(*i32 == *align(align_of_i32) i32);

    if (builtin.target.cpu.arch == .x86_64) {
        // 获取了 x86_64 架构的指针对齐大小
        try expect(@typeInfo(*i32).Pointer.alignment == 4);
    }
}

🅿️ 提示

*i32 类型可以强制转换为 *const i32 类型类似,具有较大对齐大小的指针可以隐式转换为具有较小对齐大小的指针,但反之则不然。

如果有一个指针或切片的对齐方式较小,但知道它实际上具有较大的对齐方式,请使用 @alignCast 将指针更改为更对齐的指针,例如:@as([]align(4) u8, @alignCast(slice4)),这在运行时无操作,但插入了安全检查。

示例
zig
const expect = @import("std").testing.expect;

// 全局变量
var foo: u8 align(4) = 100;

fn derp() align(@sizeOf(usize) * 2) i32 {
    return 1234;
}

// 以下是两个函数
fn noop1() align(1) void {}
fn noop4() align(4) void {}

pub fn main() !void {
    // 全局变量对齐
    try expect(@typeInfo(@TypeOf(&foo)).Pointer.alignment == 4);
    try expect(@TypeOf(&foo) == *align(4) u8);
    const as_pointer_to_array: *align(4) [1]u8 = &foo;
    const as_slice: []align(4) u8 = as_pointer_to_array;
    const as_unaligned_slice: []u8 = as_slice;
    try expect(as_unaligned_slice[0] == 100);

    // 函数对齐
    try expect(derp() == 1234);
    try expect(@TypeOf(derp) == fn () i32);
    try expect(@TypeOf(&derp) == *align(@sizeOf(usize) * 2) const fn () i32);

    noop1();
    try expect(@TypeOf(noop1) == fn () void);
    try expect(@TypeOf(&noop1) == *align(1) const fn () void);

    noop4();
    try expect(@TypeOf(noop4) == fn () void);
    try expect(@TypeOf(&noop4) == *align(4) const fn () void);
}

零指针

零指针实际上是一个未定义的错误行为(Pointer Cast Invalid Null),但是当我们给指针增加上 allowzero 修饰符后,它就变成合法的行为了!

关于零指针的使用

请只在目标 OS 为 freestanding 时使用零指针,如果想表示 null 指针,请使用可选类型

zig
// 本示例中仅仅是构建了一个零指针
// 并未使用,故可以在所有平台运行
const std = @import("std");
const expect = std.testing.expect;

pub fn main() !void {
    const zero: usize = 0;
    const ptr: *allowzero i32 = @ptrFromInt(zero);
    try expect(@intFromPtr(ptr) == 0);
}

编译期

只要代码不依赖于未定义的内存布局,那么指针也可以在编译期发挥作用!

zig
const expect = @import("std").testing.expect;

pub fn main() void {
    comptime {
        // 在这个 comptime 块中,可以正常使用pointer
        // 不依赖于编译结果的内存布局,即在编译期时不依赖于未定义的内存布局
        var x: i32 = 1;
        const ptr = &x;
        ptr.* += 1;
        x += 1;
        try expect(ptr.* == 3);
    }
}