Day 15: 结构体定义与使用
欢迎来到第十五天!在前面的课程中,我们已经学习了Zig的基本类型、数组、切片和指针。今天,我们将学习如何将这些数据组合成一个有意义的整体,这就是结构体(struct) 的用途。Struct允许你创建自己的复合数据类型,将多个相关的字段(fields)封装在一起。在Zig中,结构体是创建复杂数据模型和组织代码的基础。与面向对象语言中的类不同,Zig的结构体是纯粹的值类型,并且没有继承的概念,这使得其行为更加简单和可预测。
2. 定义结构体
Section titled “2. 定义结构体”一个结构体的定义由 struct 关键字和一对花括号组成,括号内是它的字段列表。每个字段都有一个名称和一个类型。
const Point = struct { x: f32, y: f32,};
const User = struct { id: u64, name: []const u8, is_active: bool,};按照惯例,结构体的名称使用 PascalCase(首字母大写)。
3. 创建和访问结构体实例
Section titled “3. 创建和访问结构体实例”要创建一个结构体的实例,你可以使用 . { ... } 语法。
// 创建一个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不会影响p14. 结构体方法
Section titled “4. 结构体方法”你可以为结构体定义函数,这些函数被称为方法(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.0rect.scale(2.0); // 现在 rect.width 是 20, rect.height 是 10self: *const Self(或*const Rectangle): 用于不修改结构体的方法。self: *Self(或*Rectangle): 用于需要修改结构体字段的方法。Self是一个特殊的类型,它代表当前的结构体类型,这在泛型编程中非常有用。
5. 嵌套结构体与 packed struct
Section titled “5. 嵌套结构体与 packed struct”结构体可以包含其他结构体作为字段。
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架构上是不允许的。
6. 示例:带方法的 Point
Section titled “6. 示例:带方法的 Point”让我们为 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)}7. 实践练习:Vector2D 计算
Section titled “7. 实践练习:Vector2D 计算”创建一个 Vector2D 结构体,它与 Point 类似,也包含 x: f32 和 y: f32 字段。然后为其实现以下方法:
add(self: *const Vector2D, other: Vector2D) Vector2D: 返回两个向量相加的结果(一个新的Vector2D)。dot(self: *const Vector2D, other: Vector2D) f32: 计算并返回两个向量的点积(dot product)。
8. 常见问题
Section titled “8. 常见问题”问:结构体的大小(@sizeOf)为什么有时比其所有字段大小的总和要大?
答:这就是内存对齐(memory alignment)和填充(padding)的结果。CPU访问特定类型的数据(如 u32 或 f64)时,如果其内存地址是其大小的倍数,效率会最高。为了保证这一点,编译器可能会在字段之间插入一些未使用的字节。使用 packed struct 可以禁用这种行为。
问:Zig有构造函数和析构函数吗?
答:没有。Zig没有像C++那样的自动构造函数和析构函数。惯例是创建一个名为 init 的函数作为构造函数,它返回一个初始化好的结构体实例。对于需要释放资源的结构体,惯例是提供一个 deinit 方法,并使用 defer 来确保它被调用。
今天,我们学习了如何使用 struct 来创建自己的数据类型。我们掌握了如何定义字段、创建实例、添加方法,并了解了 packed struct 在内存布局控制中的作用。结构体是Zig中组织和抽象数据的核心工具,它简单、强大且高效,并且与C的 struct 高度兼容,这使得与C代码的互操作变得轻而易举。
明天,我们将学习另外两种强大的复合类型:枚举(enum)和联合体(union)。