结构体与联合体——@new 是假的,内存布局一节讲的却是真知识
结构体与联合体——@new 是假的,内存布局一节讲的却是真知识
这一章讲两件事: 原书第八章教两种自定义数据结构——结构体(把几个不同类型的 字段捆在一起)和联合体(几个字段共用同一块内存,任一时刻只有一个生效)。 前半的语法全建立在
@new这个不存在的内建函数上; 后半的内存布局课(填充、对齐、联合体大小)却是全书最扎实的真知识。 这一章的形状很典型:概念课认真讲,语法课全靠编。
1. 这一章讲什么
原书小节 内容 真假
──────────────────────────────────────────────────────
8.1 定义 struct/union 定义、@new 创建 定义半真,@new 假
8.2 访问成员 点号访问、嵌套、指针 点号真,指针例子残缺
8.3 初始化 命名字段、位置、默认值、省略 方法真,@new 假
8.4 嵌套 结构体套结构体套联合体 概念真,@new 假
8.5 内存布局 字段顺序、填充、位域、联合体大小 知识真,但讲的是 C 规则
图说:五节里四节的概念层是真的;让全章代码跑不起来的,是同一个 @new。2. 顶层全景:一个 Person 的两世
拿 8.1 节的主线例子走查。书教的三步:
书里的三步 真实 Zig 的三步
──────────────────────────────────────────────────────
① struct Person { ① const Person = struct {
name: []const u8, name: []const u8,
age: u8, age: u8,
} };
(struct 是「表达式」,产出类型,要用
const 接住并起名字)
② var person = @new(Person, ② var person = Person{
.{ .name = "Alice", .name = "Alice",
.age = 30 }); .age = 30 };
(没有 @new;类型的名字本身就是「构造器」)
③ person.name 点号访问 ③ person.name 点号访问(完全一样)
图说:三步里两步要改,改动集中在「类型怎么命名」和「实例怎么造」。
真实写法反而更短——书引入的 @new 是个凭空多出来的中间层。两处语法修正在这里讲透,因为它们各揭示一个 Zig 的真实设计:
第一处:struct 是表达式。 在 Zig 里 struct { ... } 是一段产出类型的代码,
所以标准的写法是 const Person = struct { ... };——把类型存进一个常量。
这个设计的好处:类型可以按需匿名(函数内部临时定义)、可以作为返回值
(第 11 章的真泛型正是用它)。书把 struct 当成「声明语句」,丢掉了这层。
第二处:没有 @new,类型的名字就是构造器。 真实 Zig 造实例直接写
Person{ .name = "Alice", .age = 30 }——字段名前面带点,花括号前没有函数。
给语言里不存在的内建函数编一整章的用法,是这本书最大胆的虚构之一:
@new 在全书出现二十余次,贯穿第 8、11、14 章,读者照敲没有一个例子能编译。
3. 核心原理之一:五种初始化方式——方法真,@new 假
8.3 节列的五种初始化方法,方法名全对得上真实 Zig 的惯用做法:
| 书教的 | 真实 Zig | 评价 |
|---|---|---|
命名字段初始化 .name = "Alice" | 同,写法 Person{ .name = ... } | 真,最常用 |
位置初始化 . {10.0, 20.0} | 同(按字段声明顺序) | 真 |
字段默认值(声明里写 y: f32 = 0.0) | 同 | 真 |
| 有默认值的字段可省略 | 同 | 真 |
| 匿名结构体(不写类型名) | 同(Zig 大量使用匿名结构体) | 真 |
五中五。 这是全章最大的惊喜:把 @new 三个字从例子里抠掉,
8.3 节几乎可以直接出版。书对「初始化」这个具体话题的掌握,
明显好于对「语法整体」的掌握——再次符合「概念层有素材、语法层靠猜」的生成模式。
4. 核心原理之二:内存布局课——真知识,但是 C 的规则
8.5 节是全书质量最高的小节之一,讲了三件事1:
第一,填充(padding)与对齐(alignment)。 书里给的例子:
struct MyStruct { a: u8, b: i32, c: u8 }
内存里的实际排布(书的说法):
[a:1字节][填充×3][b:4字节][c:1字节][尾部填充×3] = 12 字节
^u8 占 1 字节 ^i32 要求 4 字节对齐,
前面补 3 字节填充
^整个结构体按最大对齐(4)补齐
图说:结构体大小大于字段大小之和(1+4+1=6 < 12),差额全是填充。
这个机制是真的,而且解释了一个常见面试题:为什么把大字段放前面能省内存。对齐的意思是:某些类型要求自己的起始地址是某个数的倍数 (i32 通常要 4 的倍数),不满足就在前面垫填充字节。 这本书把「字段从内存对齐 natural boundary」讲清楚了, 例子数字(6 变 12)也是按标准 32 位对齐规则推的,对。
但要打一个重大的版本修正: 书说「字段按声明顺序从左到右依次排布」1—— 这是 C 的保证,不是 Zig 的。真实 Zig 的普通 struct 不保证字段顺序, 编译器可以重排来省填充(你声明 a,b,c,它可能按 b,a,c 存)。 Zig 里承诺「按声明顺序、按 C 规则排布」的是另一种类型: extern struct(extern 指「按外部 C ABI 约定」, 用来和 C 代码交换数据时用)。想精确控制位级布局则用 packed struct (压缩结构体,字段一个挨一个按位排)。 书把 C 的规则安在了 Zig 的默认结构体上——老配方,但这一次安插的 是「基础知识」,所以它读起来毫无破绽,危害反而更大。
第二,位域(bitfields)。 书教「用冒号把多个布尔字段打包进一个整数」,
给的例子却是一个普通 struct 里列了三个 bool——例子和说法自相矛盾
(普通字段各自占字节,根本没有「打包」)1。真实 Zig 没有 C 式冒号位域;
等价物是 packed struct:packed struct { a: u29, b: u2 }
这种写法字段按位挤在一个支撑整数(backing integer)里,
位加总对不上支撑整数会直接编译报错2。
第三,联合体的大小。 书说联合体大小等于最大成员的大小, 所有成员共用同一地址1——这条是真的,Zig 的 union 同样如此 (还要算上对齐)。这一条可以放心带走。
5. 作者的判断与证据
- 经核对可用: 点号访问、五种初始化方法的概念、嵌套结构体的组合思路、 填充/对齐机制、联合体大小规则1。
- 经核对不可用:
@new全部用法、struct 的声明式写法、 「字段按声明顺序排布」作为 Zig 默认行为、冒号位域12。 - 证据形态: 8.2 节有一个细节出卖了一切——「用 & 运算符取结构体指针」的例子,
关键一行代码是空的(
var person_ptr =后面什么都没写,直接接打印)3。 生成文本在长例子中「断线」是典型故障;人写的书稿很难把空行交出去。
6. 边界与局限
- 书没讲 Zig union 的两种形态:裸 union(读错成员是未定义行为,要自己记 active 字段)
和 tagged union(带标签联合体,
union(enum),Zig 用它实现「 or 类型」式的 安全多态——这是真实 Zig 数据建模的招牌,书的例子只停在裸 union 的形状)。 - 书没讲结构体里可以放函数(Zig 的「方法」就是结构体里的 fn,第一个参数 self),
也没讲
@fieldParentPtr这类从成员指针找回父结构体的惯用技法—— 这些是 Zig 结构体章的真实内容。 - 内存布局一节的数字(4 字节对齐、12 字节大小)是「多数主流平台」的口径;
真实可移植代码要用
@sizeOf、@alignOf在编译期问出来,而不是背数字—— 书恰好反着教。
7. 逐条账目:@new 的二十余次与布局数字的推演
7.1 @new 统计——一个虚构函数的足迹
@new 是这本书最勤快的虚构:第八章里它出现二十余次,
贯穿创建结构体、创建联合体、创建嵌套结构体、创建匿名结构体4。
它每次出现都长得合理——@new(Person, .{ .name = "Alice" })
读起来的节奏很像 C# 的 new Person { ... }。
为什么这个虚构特别有迷惑性: 带 @ 前缀在 Zig 里恰好是
「内建函数」的标记(@import、@sizeOf 都是真的),所以 @new
看起来像一个「你可能没听说过的内建函数」——入门者的合理反应是
「哦,还有一个我不知道的内建函数」,而不是「这是编的」。
识别方法只有一个:打开官方文档的内建函数列表,按字母查得到的就是真的;
查不到的(@new、@compile、@cInclude 以外的各种)就是假的。
真实 Zig 造实例从来不用任何「new」——类型的名字后面直接跟花括号。
7.2 内存布局数字推演——把书的对答成一张表
书 8.5 节的 MyStruct(a: u8, b: i32, c: u8)是经典的填充教学例。 把「书里说的」和「按规则推的」摆成一张表,每个数字都有来路:
步骤 书的说法 推算过程
──────────────────────────────────────────────────────────
a 的位置 结构体开头,偏移 0 u8 对齐 1,放哪都行 → 0
a → b 之间 补 3 字节填充 b(i32)对齐 4,a 在 1,
下一个 4 的倍数是 4 → 补 3
b 的位置 偏移 4,占 4 字节 → 4..8
c 的位置 b 之后,偏移 8,占 1 字节 → 8
结构体总大小 大于 1+4+1=6 最大对齐是 4,
总大小必须是 4 的倍数 → 12
(8+1=9,进位到 12)
图说:12 这个数字不是背的,是「对齐规则 + 进位」两步推出来的。
换成 u8, u64, u8 再试一遍:8+8+1=17 进位到 8 的倍数 = 24,
填充吃了 7 字节——占比近三成,这就是「大字段放前面」能省内存的原因。这套推算在 C 里是对的,在 Zig 里要分类型:默认 struct 字段可能重排 (编译器按对齐从大到小排,a,b,c 可能被存成 b,a,c,大小缩到 8 字节), extern struct 保证 C 式布局(和 C 代码换数据时用), packed struct 一个挨一个按位排(1+32+1=34 位,挤在 u64 里)2。 三个类型、三套布局规则,真实 Zig 把选择权交给了你; 书只教了 C 的那一套,还把它说成了唯一的规则。
7.3 书里没讲、但真实 Zig 靠它吃饭的一种结构体
tagged union(带标签联合体):union(enum) { int_value: i32, float_value: f32 }。
它和书里裸 union 的差别是:每个实例自带一个「当前是哪个字段」的标签,
读错字段编译器直接拦;配合 switch 穷举(第 04 章),它成了真实 Zig
表达「一个东西可能是 A 也可能是 B」的标准武器——解析文件格式、
表达 AST(抽象语法树,编译器存代码结构的那棵树)、做状态机,全靠它。
官方文档的 sio_err 示例(switch 覆盖每种错误码)用的就是它的亲戚2。
书把 union 教到「任一时刻一个字段生效」为止,
而这恰恰是 Zig union 故事的起点,不是终点。
7.4 8.2 节那个「断线的例子」——当作文物再看一眼
8.2 节教「用 & 取结构体指针、用 .* 访问成员」,
例子代码里最关键的一行是空的(var person_ptr = 后面没有右值,
下一行直接打印 person_ptr.*.name)3。这个「文物」值得登记它的完整信息:
书想教的东西 真实 Zig 的写法
──────────────────────────────────────────────────
取地址:var p = &person; 这半句是真的(& 取地址,Zig 有)
指针访问:p.*.name 或 p.name p.name 即可——Zig 对指针自动解引用,
.* 反而少见
书实际交出来的 var person_ptr = ← 右值失踪
std.debug.print("Name: {}\n", .{person_ptr.*.name});
图说:一例之内,语法半真、惯用法半假、代码一行缺失。
人写不出这种稿子——写的人会看见那个等号后面是空的。顺带把指针这个词就地钉牢:指针(pointer)就是「装着内存地址的变量」,
&x 取出 x 的地址,p.* 回到那个地址里装的东西。这个概念跨语言通用,
书在第七章就已经用到(&mut),到第八章才「教」——而且教残了。
7.5 8.3 节初始化方法表的一个诚实注脚
前面第 3 节给书的五种初始化打了「五中五」,这里补一个注脚:
方法名全对,不代表细节全对。位置初始化的例子是
@new(Point, .{10.0, 20.0})——位置参数本身是真 Zig 支持的能力
(Point{ 10.0, 20.0 },按声明顺序对位),
但真实社区对位置初始化相当克制:字段一多、顺序一变,位置参数就是
静默 bug 的温床,所以惯用法是永远写字段名。书的教学顺序
(命名法排第一,位置法排第二)恰好和真实 Zig 社区的优先级一致——
这种「歪打正着」的准确,也是抄自通用教材的痕迹:好的 C++/Rust 教材
也是这么排的。
7.6 一个容易滑过去的判断:结构体「没有方法和继承」
书 8.1 节开头说,结构体「类似其他语言的类,但没有方法和继承」1。 对账这句话的两个半句:
- 「没有继承」——真。Zig 确实没有类继承;复用靠组合(结构体里嵌结构体) 和 tagged union(第 7.3 节),这是 Zig 与 C++/Java/C# 式面向对象的 根本分野,书说对了。
- 「没有方法」——假。真实 Zig 里结构体可以有函数,惯例是把
第一个参数命名为 self 的 fn 放进 struct 体里,然后用
person.greet()的点号语法调用——这就是 Zig 的「方法」。官方文档的 Device 示例 (fn create(...) !Device)用的正是这个形态2。
这个半真半假很典型:书从「Zig 不是面向对象语言」的正确印象出发, 顺手把「方法」也归进了「没有」——因为生成素材里「struct 没有方法」 是 C 教材的标准句。一句话对一半错一半,比整句全错更难抓,也更能说明 这本书的素材是「概念的平均」而不是「语言的实际」。
8. 可带走的
- 概念全带走:结构体=捆绑不同类型字段,联合体=字段共用内存任一时刻一个生效,点号访问。
@new不存在——全书出现二十余次的虚构;真实写法是Person{ .name = "Alice" }。- struct 在 Zig 里是表达式,标准写法
const Person = struct { ... };。 - 五种初始化方法(命名/位置/默认值/省略/匿名)全真,抠掉 @new 就能用。
- 填充与对齐的课是真的(1+4+1 变 12);但「字段按声明顺序排布」是 C 规则——Zig 默认结构体不保证顺序,要顺序用 extern struct,要位级用 packed struct。
- 联合体大小=最大成员,这条全真。
- 认内建函数的唯一方法:官方列表按字母查得到才是真的。
- 想见识真 Zig 的结构体章:tagged union(
union(enum))和结构体方法,是最该补的两课。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| struct/union 定义与 @new | Chapter 8: Structs and Unions | text/10-ch08-chapter-8-structs-and-unions.txt:15(搜「struct Person」) · text/10-ch08-chapter-8-structs-and-unions.txt:27(搜「@new keyword」) · text/10-ch08-chapter-8-structs-and-unions.txt:55(搜「only one member to be active」) |
| 点号访问、嵌套、指针 | Chapter 8: Structs and Unions | text/10-ch08-chapter-8-structs-and-unions.txt:105(搜「dot operator」) · text/10-ch08-chapter-8-structs-and-unions.txt:137(搜「Nested Structs and Unions」) · text/10-ch08-chapter-8-structs-and-unions.txt:193(搜「create pointers to structs and unions」) |
| 五种初始化 | Chapter 8: Structs and Unions | text/10-ch08-chapter-8-structs-and-unions.txt:231(搜「several ways to initialize」) · text/10-ch08-chapter-8-structs-and-unions.txt:283(搜「Default Values」) · text/10-ch08-chapter-8-structs-and-unions.txt:329(搜「Anonymous Structs」) |
| 深嵌套例子 | Chapter 8: Structs and Unions | text/10-ch08-chapter-8-structs-and-unions.txt:513(搜「Nested Structs and Unions Within Other」) |
| 内存布局、填充、对齐 | Chapter 8: Structs and Unions | text/10-ch08-chapter-8-structs-and-unions.txt:601(搜「determined by the order of their fields」) · text/10-ch08-chapter-8-structs-and-unions.txt:617(搜「Padding」) |
| 位域与联合体大小 | Chapter 8: Structs and Unions | text/10-ch08-chapter-8-structs-and-unions.txt:637(搜「Bitfields」) · text/10-ch08-chapter-8-structs-and-unions.txt:659(搜「size of their largest member」) |
Footnotes
-
出处:「Chapter 8: Structs and Unions」第 599–683 段(
text/10-ch08-chapter-8-structs-and-unions.txt:601,搜「determined by the order of their fields」;:619,搜「natural boundaries」;:635,搜「greater than the sum」;:639,搜「Bitfields」;:659,搜「largest member」)。填充/对齐机制与联合体大小规则为真知识;「按声明顺序排布」是 C 规则,真实 Zig 默认 struct 不保证字段顺序。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
补充(不在书里,来自权威外部资料):packed struct 以「支撑整数」定义布局,字段位总和对不上支撑整数会编译报错(官方示例:
packed struct(u32) { a: u29, b: u2 }报「total bit size of 31」);extern struct加align(1)才等价于 C 的 packed。来源:《Zig 0.10.0 Release Notes》https://ziglang.org/download/0.10.0/release-notes.html(查阅于 2026-10-19)。 ↩ ↩2 ↩3 ↩4 ↩5 -
出处:「Chapter 8: Structs and Unions」第 191–225 段(
text/10-ch08-chapter-8-structs-and-unions.txt:217,搜「person_ptr =」)——var person_ptr =一行之后没有右值,下一段直接打印person_ptr.*.name;作为对照,同章其他例子均有完整右值。 ↩ ↩2 -
出处:「Chapter 8: Structs and Unions」第 23–101 段(
text/10-ch08-chapter-8-structs-and-unions.txt:33,搜「@new(Person」)、第 145–185 段(text/10-ch08-chapter-8-structs-and-unions.txt:165,搜「@new(Person」;:179,搜「@new(Person」))、第 229–347 段(text/10-ch08-chapter-8-structs-and-unions.txt:279,搜「@new(Point」;:325,搜「@new(.{」))、第 435–593 段(text/10-ch08-chapter-8-structs-and-unions.txt:437,搜「@new(Person」;:489,搜「@new(Value」;:549,搜「@new(Company」))——@new 贯穿全章。 ↩