Skip to content

Day 15: 结构体定义与使用

欢迎来到第十五天!在前面的课程中,我们已经学习了Zig的基本类型、数组、切片和指针。今天,我们将学习如何将这些数据组合成一个有意义的整体,这就是结构体(struct) 的用途。Struct允许你创建自己的复合数据类型,将多个相关的字段(fields)封装在一起。在Zig中,结构体是创建复杂数据模型和组织代码的基础。与面向对象语言中的类不同,Zig的结构体是纯粹的值类型,并且没有继承的概念,这使得其行为更加简单和可预测。

一个结构体的定义由 struct 关键字和一对花括号组成,括号内是它的字段列表。每个字段都有一个名称和一个类型。

const Point = struct {
x: f32,
y: f32,
};
const User = struct {
id: u64,
name: []const u8,
is_active: bool,
};

按照惯例,结构体的名称使用 PascalCase(首字母大写)。

要创建一个结构体的实例,你可以使用 . { ... } 语法。

// 创建一个Point实例
var p1 = Point{ .x = 10.5, .y = -3.2 };
// 字段的顺序不重要
const p2 = Point{ .y = 1.0, .x = 0.0 };
// 如果所有字段都按顺序提供,可以省略字段名
// 但为了代码清晰,不推荐这样做
const p3 = Point{ 2.0, 3.0 };
// 访问和修改字段
p1.x = 11.0;
const user_name = user.name;

结构体是值类型,这意味着当你将一个结构体实例赋值给另一个变量时,会创建该结构体的一个完整副本。

var p4 = p1; // p4是p1的一个副本
p4.x = 0; // 修改p4不会影响p1

你可以为结构体定义函数,这些函数被称为方法(methods)。方法就是定义在 struct 的命名空间内的函数。第一个参数通常是 self,它是一个指向结构体实例的指针。

const Rectangle = struct {
width: f32,
height: f32,
// 这是一个方法
// self: *const Rectangle 表示这是一个只读方法
fn area(self: *const Rectangle) f32 {
return self.width * self.height;
}
// self: *Rectangle 表示这是一个可变方法
fn scale(self: *Rectangle, factor: f32) void {
self.width *= factor;
self.height *= factor;
}
};
// 调用方法
var rect = Rectangle{ .width = 10, .height = 5 };
const a = rect.area(); // a 是 50.0
rect.scale(2.0); // 现在 rect.width 是 20, rect.height 是 10
  • self: *const Self (或 *const Rectangle): 用于不修改结构体的方法。
  • self: *Self (或 *Rectangle): 用于需要修改结构体字段的方法。
  • Self 是一个特殊的类型,它代表当前的结构体类型,这在泛型编程中非常有用。

结构体可以包含其他结构体作为字段。

const Line = struct {
start: Point,
end: Point,
};

默认情况下,编译器可能会在字段之间插入填充(padding)以优化对齐,从而提高访问速度。如果你需要精确控制内存布局,不希望有任何填充(例如,为了与硬件或网络协议交互),你可以使用 packed struct。

const PackedExample = packed struct {
a: u8, // 1 byte
b: u16, // 2 bytes
};
// @sizeOf(PackedExample) == 3
const NormalExample = struct {
a: u8,
b: u16,
};
// @sizeOf(NormalExample) 可能是 4

警告:访问 packed struct 中未对齐的字段可能会非常慢,甚至在某些CPU架构上是不允许的。

让我们为 Point 结构体添加一些有用的方法。

const std = @import("std");
const Point = struct {
x: f32,
y: f32,
// 计算到原点的距离
fn magnitude(self: *const Point) f32 {
return std.math.sqrt(self.x * self.x + self.y * self.y);
}
// 将点移动指定的距离
fn translate(self: *Point, dx: f32, dy: f32) void {
self.x += dx;
self.y += dy;
}
};
pub fn main() !void {
var p = Point{ .x = 3.0, .y = 4.0 };
std.debug.print("Initial magnitude: {d}\n", .{p.magnitude()}); // 5.0
p.translate(1.0, 1.0);
std.debug.print("New position: ({d}, {d})\n", .{ p.x, p.y }); // (4.0, 5.0)
}

创建一个 Vector2D 结构体,它与 Point 类似,也包含 x: f32 和 y: f32 字段。然后为其实现以下方法:

  1. add(self: *const Vector2D, other: Vector2D) Vector2D: 返回两个向量相加的结果(一个新的 Vector2D)。
  2. dot(self: *const Vector2D, other: Vector2D) f32: 计算并返回两个向量的点积(dot product)。

问:结构体的大小(@sizeOf)为什么有时比其所有字段大小的总和要大?

答:这就是内存对齐(memory alignment)和填充(padding)的结果。CPU访问特定类型的数据(如 u32 或 f64)时,如果其内存地址是其大小的倍数,效率会最高。为了保证这一点,编译器可能会在字段之间插入一些未使用的字节。使用 packed struct 可以禁用这种行为。

问:Zig有构造函数和析构函数吗?

答:没有。Zig没有像C++那样的自动构造函数和析构函数。惯例是创建一个名为 init 的函数作为构造函数,它返回一个初始化好的结构体实例。对于需要释放资源的结构体,惯例是提供一个 deinit 方法,并使用 defer 来确保它被调用。

今天,我们学习了如何使用 struct 来创建自己的数据类型。我们掌握了如何定义字段、创建实例、添加方法,并了解了 packed struct 在内存布局控制中的作用。结构体是Zig中组织和抽象数据的核心工具,它简单、强大且高效,并且与C的 struct 高度兼容,这使得与C代码的互操作变得轻而易举。

明天,我们将学习另外两种强大的复合类型:枚举(enum)和联合体(union)。