指针
zig 作为一门 low level 语言,那肯定要有指针的。
指针是指向一块内存区域地址的变量,它存储了一个地址,我们可以通过指针来操作其指向内存区域。
取地址:通过 &
符号来获取某个变量所对应的内存地址,如 &integer
就是获取变量 integer
的内存地址。
zig 的指针和 C 的指针略有不同,包含两种指针,一种单项(single-item)指针,一种是多项(many-item)指针,它们的解引用的方式也略有不同。
关于指针运算
zig 本身支持指针运算(加减操作),但有一点需要注意:最好将指针分配给 [*]T
类型后再进行计算。
尤其是在切片中,不可直接对其指针进行更改,这会破坏切片的内部结构!
单项指针
单项指针指向单个元素。
单项指针的类型为:*T
,T
是所指向内存区域的类型,解引用方法是 ptr.*
。
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;
多项指针
多项指针指向位置数量的多个元素。
多项指针的类型为:[*]T
,T
是所指向内存区域的类型,且该类型必须具有明确的大小(这意味着它不能是 anyopaque
和其他任意不透明类型)。
解引用方法支持以下几种:
- 索引语法
ptr[i]
- 切片语法
ptr[start..end]
- 指针运算
ptr + x
,ptr - x
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
属性来获取。
示例
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 交互的特性,故你可以暂时略过这里!
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
的值,以避免无法正确触发 “副作用”),并保证了代码执行的顺序。
// 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 中,指针类型具有对齐值。如果该值等于基础类型的对齐方式,则可以从类型中省略它:
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))
,这在运行时无操作,但插入了安全检查。
示例
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
指针,请使用可选类型!
// 本示例中仅仅是构建了一个零指针
// 并未使用,故可以在所有平台运行
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);
}
编译期
只要代码不依赖于未定义的内存布局,那么指针也可以在编译期发挥作用!
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);
}
}