编程语言研究书库

结构体与联合体——@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. 可带走的

  1. 概念全带走:结构体=捆绑不同类型字段,联合体=字段共用内存任一时刻一个生效,点号访问。
  2. @new 不存在——全书出现二十余次的虚构;真实写法是 Person{ .name = "Alice" }。
  3. struct 在 Zig 里是表达式,标准写法 const Person = struct { ... };。
  4. 五种初始化方法(命名/位置/默认值/省略/匿名)全真,抠掉 @new 就能用。
  5. 填充与对齐的课是真的(1+4+1 变 12);但「字段按声明顺序排布」是 C 规则——Zig 默认结构体不保证顺序,要顺序用 extern struct,要位级用 packed struct。
  6. 联合体大小=最大成员,这条全真。
  7. 认内建函数的唯一方法:官方列表按字母查得到才是真的。
  8. 想见识真 Zig 的结构体章:tagged union(union(enum))和结构体方法,是最该补的两课。

9. 原文地图

主题原书章原文位置
struct/union 定义与 @newChapter 8: Structs and Unionstext/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 Unionstext/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 Unionstext/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 Unionstext/10-ch08-chapter-8-structs-and-unions.txt:513(搜「Nested Structs and Unions Within Other」)
内存布局、填充、对齐Chapter 8: Structs and Unionstext/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 Unionstext/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

  1. 出处:「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

  2. 补充(不在书里,来自权威外部资料):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

  3. 出处:「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

  4. 出处:「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 贯穿全章。 ↩

On this page