跳到主要内容
🔍
1.3.6已发布intermediate · 约 4 课时 · P0

结构体与位段

掌握结构体与内存对齐规则,理解位段在寄存器配置中的应用与其可移植性边界

  • 来源
  • 补充
  • 纠错

标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原

完成标准(本章)

  • 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 240
  • ✏️ 已练习:小练习正确率 ≥ 60%
  • 📝 已通过测验:分数 ≥ 60
  • 🛠️ 已掌握还需完成实践任务
学习状态:未开始

1.3.6 结构体与位段

本章来源:结构体定义/初始化/访问、结构体数组与指针、传值传址、位段语法来自《第一阶段讲义》21.x 节【来源】,经重新组织表述;位段"int 最多 31 位预留符号位"表述已【纠错】(err-7.3-13:宽度上限与布局均为实现定义);内存对齐规则(offsetof/pack 实测)、柔性数组与"序列化不能依赖内存布局"为新编【补充】(约 20%)。平台适用性 universal——但本章所有 sizeof/offsetof 数值均为本机实测,换平台数值不同。

① 学习目标

  1. 定义结构体并用逐个初始化、聚合初始化与 C99 指定初始化创建对象;
  2. .-> 正确访问结构体成员(含结构体数组与嵌套结构体);
  3. 解释内存对齐:为什么 sizeof(struct) 常常大于成员之和,并会用 offsetof 实测;
  4. 说出结构体传值与传址的区别(拷贝代价与修改语义),并会按场景选择;
  5. 用位段定义寄存器结构并读写,同时说清位段布局的实现定义性质与可移植性边界;
  6. 知道柔性数组成员(C99)的用途,并明白序列化不能直接依赖结构体的内存布局

② 前置知识

  • 必选:1.3.2 指针与数组(-> 是"解引用 + 成员访问"的语法糖;结构体数组与指针访问都以它为基础)。

③ 核心概念【来源】

概念说明
struct 定义与对象struct Point { int x; int y; }; 只是类型;struct Point p; 才是对象;类型不占内存,对象才占
初始化逐个 p.x=1;、聚合 {1,2}、C99 指定初始化 {.x=1, .y=2}(未指定的成员自动补 0)
成员访问对象用 .p.x);指针用 ->ps->x,等价于 (*ps).x
内存对齐每个成员按其类型的对齐要求放在"自身对齐值的整数倍"地址上,成员之间和结构体末尾可能插入填充字节(padding)——这是 sizeof(struct) > 成员之和 的原因;对齐是实现定义的(本机 x86_64 MinGW:int 对齐 4)
offsetof<stddef.h> 的宏:成员相对结构体起始的字节偏移——对齐规则的"照妖镜"
pragma pack#pragma pack(push, 1) 取消填充(压缩布局);代价:可能产生非对齐访问(部分平台更慢甚至非法)、降低可移植性——只用于网络报文/寄存器映射等有明确布局要求的场景
柔性数组成员结构体最后一个成员写成 int data[];(C99):结构体本身不含数组空间,需整体 malloc 一块连续内存;适合"长度可变的报文头+负载"
位段unsigned int enable : 1; 按位打包成员;布局(位顺序/跨存储单元/填充)是实现定义的(err-7.3-13);工程上只用于寄存器映射与协议字段,且用 unsigned 类型
传值 vs 传址传值复制整个结构体(大结构体拷贝代价高、函数内修改不影响实参);传址只传指针(省拷贝、可修改、但被调方能改实参)

④ 通俗解释【补充】

  • 结构体是"行李箱":char 是张纸片、int 是本书、double 是台仪器;对齐就是机场货舱规定"书必须放在 4 的倍数格子里、仪器放在 8 的倍数格子里"——为了摆放整齐,格子之间会空出缝隙(padding),箱子总尺寸自然比物品体积之和大;
  • offsetof 是卷尺:能量出每个物品实际放在箱子的哪个格位;
  • pragma pack 是"压缩打包":把缝隙全挤掉省运费,但仪器可能被放在歪格子上——有的仓库(平台)不介意,有的直接拒收(非对齐访问异常),所以只在确有必要时用;
  • 位段是"格子里再分小隔间":一个 4 字节格子切成 1 位+2 位+4 位+1 位四个小隔间,放 4 个开关值;但小隔间怎么排(从左还是从右)各厂标准不同——读同一份配置前必须确认本平台的排法
  • 序列化(把结构体写进文件/网络)不能直接整块拷贝:箱子的"摆放规则"(对齐/填充/字节序)各平台不同,A 平台打包的箱子 B 平台拆不开——必须逐字段转换(如 ex 中的 memcpy 逐字节),这就是"内存布局 ≠ 传输格式"。

⑤ 示例代码

代码示例与验证记录

  • examples/ex1-struct-basics.c结构体基础(聚合/指定初始化 + 数组与指针访问)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16
    编译:gcc ex1-struct-basics.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic
    适用环境:LinuxWindows(MinGW)
    展开预期输出(实测)
    p=(3,4) q=(10,20)
    student: Alice id=1001 score=95
    arr[2]=(3,3) 数组总大小=24
    

    差异说明:本机实测(0 警告);数组总大小 24 = 3 个 Point × 8 字节(两个 int 无填充)

    完整源码见 /code 代码示例页

  • examples/ex2-struct-align.c内存对齐实测(offsetof 与 pragma pack)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16
    编译:gcc ex2-struct-align.c -o ex2 -std=c11 -Wall -Wextra -Wpedantic
    适用环境:LinuxWindows(MinGW)
    展开预期输出(实测)
    sizeof(char)=1 sizeof(int)=4
    sizeof(struct Mixed)=12(成员之和为 6,多出的是对齐填充)
    offsetof(Mixed): c=0 i=4 d=8(i 对齐到 4 的倍数)
    m = {A, 42, B}
    sizeof(struct Packed)=6(pack(1) 取消填充)
    offsetof(Packed): c=0 i=1 d=5
    

    差异说明:本机 x86_64 MinGW gcc 15.2.0 实测(0 警告);对齐是实现定义的,换平台(如 ARM)数值可能不同——本章不把这些数值当标准固定值

    完整源码见 /code 代码示例页

  • examples/ex3-bitfield-reg.c位段定义 8 位控制寄存器(本机布局实测)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16
    编译:gcc ex3-bitfield-reg.c -o ex3 -std=c11 -Wall -Wextra -Wpedantic
    适用环境:LinuxWindows(MinGW)
    展开预期输出(实测)
    sizeof(struct CtrlReg)=4(8 个位段合入 1 个 unsigned int)
    enable=1 mode=2 prescale=5 reserved=0
    

    差异说明:本机实测(0 警告);位段布局(位顺序等)为实现定义,本输出仅代表本平台/本编译器

    完整源码见 /code 代码示例页

  • examples/ex4-bitfield-portability.txt位段可移植性边界(conceptual:静态讲解,不编译不运行)概念讲解示例✓ 文档核对(非执行)· C11 标准 6.7.2.1 条文核对, 2026-08-16
    编译:无(静态讲解文本)
    适用环境:LinuxWindows(MinGW)
    展开文档核对记录
    (无运行输出——本示例为静态讲解文本,见文件内容)
    

    差异说明:内容按 C11 6.7.2.1(结构与位段约束)核对;结论与勘误 err-7.3-13 一致

    完整源码见 /code 代码示例页

⑥ 编译与运行方法

全部示例为本机实测(x86_64 MinGW gcc 15.2.0 / Windows 11,-std=c11 -Wall -Wextra -Wpedantic 编译 0 警告):

gcc ex1-struct-basics.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic
gcc ex2-struct-align.c  -o ex2 -std=c11 -Wall -Wextra -Wpedantic
gcc ex3-bitfield-reg.c  -o ex3 -std=c11 -Wall -Wextra -Wpedantic
  • ex2 是本机对齐实测:sizeof(struct Mixed)=12(1+4+1 却占 12)、offsetof 显示 i 在偏移 4、pack(1) 后 6 字节——这些数值是本机结果,换平台可能不同(实现定义);
  • ex3 位段寄存器:enable/mode/prescale/reserved 读写输出为本机布局实测;位顺序换编译器/平台可能不同(err-7.3-13)。

⑦ 常见错误

症状原因解决
【纠错】按"位段最多 31 位"写代码资料表述为某实现限制,非语言规则位段宽度上限实现定义;可移植代码用 unsigned int 位段(err-7.3-13)
sizeof(struct) 与手算成员之和不等对齐填充(padding)用 offsetof 实测每个成员偏移;不要手算结构体大小
直接 fwrite/发送整个结构体,换平台/换版本解析乱内存布局(对齐/填充/字节序/位段顺序)是实现定义的,不是传输格式序列化逐字段转换(见 6.1 网络字节序与本章 ex 的 memcpy 思路)
传值修改结构体,调用方没变化传值复制了一份,函数内改的是副本需要修改实参就传指针(-> 访问)
大结构体传值导致性能差每次调用整块拷贝只读大结构体传 const struct X *(指针)
pack(1) 后程序在某些平台崩溃/变慢非对齐访问pack 只用于明确布局要求的场景;否则保持自然对齐
位段赋超出宽度的值超出位段能表示的范围位段只能存宽度内的值(1 位段存 0/1),超范围行为是实现定义的

⑧ 小练习

小练习

学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)
  • ex-1-3-6-1.struct Mixed { char c; int i; char d; }; 在本机 x86_64(int 对齐 4)上 sizeof(struct Mixed) 是 12 而不是 1+4+1=6,原因是?(单选)

    ◌ 未作答
  • ex-1-3-6-2.要修改函数外的一个大结构体实参,最合适的做法是?(单选)

    ◌ 未作答
  • ex-1-3-6-3.代码审查:同事直接把 struct 用 fwrite 整块写入配置文件,并注释『结构体就是内存,直接存最省事』。这个做法的隐患是?(单选)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

用位段定义 8 位寄存器结构并读写

用位段定义一个 8 位控制寄存器结构(1 位使能 + 2 位模式 + 4 位分频 + 1 位保留), 按三个场景设置寄存器并打印各字段值;再用 offsetof 打印一个含 char/int 的结构体的成员偏移, 写出"为什么 sizeof(struct) 常常大于成员之和"的解释。

输入与输出

无程序输入。交付物:① 寄存器结构读写程序(三个场景输出正确);② 对齐实测程序输出; ③ 100 字以上的对齐原理解释。

功能要求

  • 位段全部用 unsigned int,宽度分别为 1/2/4/1
  • 至少设置三种组合(如:只使能、使能+模式 2、使能+模式 3+分频 10)并打印各字段
  • 用 offsetof 打印 struct Mixed { char c; int i; char d; } 的三个成员偏移与本机 sizeof
  • 写出对齐原理解释(提到"对齐要求/填充字节/实现定义"三个要点)

限制条件

  • 位段布局为实现定义:输出前注明"本机实测";不得声称这是标准固定值
  • 实验在专用目录进行,不留构建产物

验收步骤(自检清单 0/3

验收标准

  • 三个场景的字段值输出正确(验收步骤 1)
  • offsetof 输出与本机实测一致且解释提到三个要点(验收步骤 2/3)

常见失败原因

  • 位段类型用了 int 导致符号位参与造成意外
  • 把本机 sizeof/offsetof 数值当成跨平台固定值写进注释
  • 解释里漏掉'末尾 padding'只提了成员间填充

可选扩展

  • 用 #pragma pack(1) 对比同一结构体的大小变化并说明代价
  • 用移位+掩码宏重写同一寄存器读写,对比两种写法

完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)

⑪ 面试问题

面试问题

  • 为什么 sizeof(struct) 常常大于各成员 sizeof 之和?能用什么手段观察和改变它?高频C/C++ · medium

    要点:因为内存对齐:成员按自身对齐要求摆放在整数倍地址上,产生填充字节;用 offsetof 观察各成员偏移,用

    每个类型有对齐要求(如本机 int 对齐 4),成员地址必须是其对齐值的整数倍,成员之间与 结构体末尾会插入填充字节(padding),因此 sizeof(struct) 可能大于成员之和 (如 char+int+char 本机为 12 而非 6)。观察手段:offsetof 打印每个成员的偏移。 改变手段:#pragma pack(push,1) 取消填充(体积变小),但可能造成非对齐访问—— 性能下降、部分平台异常、可移植性降低,只在网络报文/寄存器映射等明确布局场景使用。 注意对齐规则是实现定义的,不同平台数值不同,不能假设。

    追问:
    • 追问:为什么对齐后访问更快?(CPU 按对齐字长取数,跨边界要两次取数或直接异常)
    评分要点:
    • 对齐与填充成因
    • offsetof 与 pack 两种手段及代价
    • 实现定义与不可假设
  • 位段(bit-field)适合做什么?它的可移植性边界在哪里?高频C/C++ · medium

    要点:适合寄存器映射与协议字段(按位打包);布局(位顺序/跨单元/填充)是实现定义的,跨平台不可假设。

    位段用 unsigned int 把多个小字段打包进一个存储单元,适合"8 位寄存器:1 位使能+2 位模式 +4 位分频+1 位保留"这类硬件寄存器映射,以及协议头部的位字段。 可移植性边界:C11 只保证类型限制与"按声明顺序分配存储",位顺序(低位在前还是高位在前)、 是否允许跨存储单元、填充都是实现定义的(err-7.3-13 修正了"最多 31 位"的表述——那是某 实现的限制)。工程做法:映射寄存器前用已知值自检本平台位顺序;或用移位+掩码宏显式表达, 完全摆脱实现定义;位段结构体不用于序列化传输。

    追问:
    • 追问:为什么用 unsigned int 位段而不是 int?(避免符号位参与造成意外与实现差异)
    评分要点:
    • 寄存器映射用途
    • 实现定义清单(位顺序等)
    • 自检或移位掩码替代方案
  • 函数参数用结构体传值和传指针有什么区别?分别适合什么场景?高频C/C++ · easy

    要点:传值整体拷贝(大结构体代价高、函数内修改不影响实参);传指针只拷贝地址(省拷贝、可改实参、有被修改风险)。

    传值:实参被完整复制进函数栈帧,函数内修改的是副本,调用方不受影响;结构体越大拷贝越贵。 传址:只传一个指针(4/8 字节),函数内通过 -> 修改的是实参本身;代价是"谁都能改"—— 只读场景应加 const(const struct X *p)表达"不改"并防误改。 选型:小结构体(如点坐标)传值简单直观;大结构体/需要修改实参/嵌入式栈空间紧张时传指针; 只读大结构体传 const 指针是默认选择。另外注意:传值是浅拷贝——内含指针成员时, 只复制指针本身不复制指向的数据(配合 1.3.9 动态内存理解深浅拷贝)。

    追问:
    • 追问:结构体含指针成员时,传值会复制指向的数据吗?(不会,浅拷贝只复制指针)
    评分要点:
    • 拷贝代价对比
    • 修改语义与 const
    • 浅拷贝提示

⑫ 延伸阅读

  • 《C 语言程序设计:现代方法》第 16 章 结构、联合与枚举——只引书名;
  • C11 标准 6.7.2.1(结构与位段)、6.5.2.3(成员访问)——标准原文为准;
  • 本地手册:man offsetof(Linux)/ <stddef.h> 头文件注释;
  • 下一章预告:1.3.7 共用体与大小端实战——union 共享内存 + 字节序判断(6.1.2 网络字节序的前置)。

迁移训练(migration training)

把本章技能迁移到 ARM 32 位板卡网络协议代码

环节本机 x86_64(MinGW)ARM 32 位网络协议
int 对齐4(实测)通常 4不适用
8 字节类型对齐8可能 4不适用
sizeof/offsetof 数值本章 ex2 实测值以目标平台实测为准不适用
位段布局本机实测排法可能不同(实现定义)协议字段按协议规定
序列化不依赖内存布局(逐字段)同左按网络字节序(6.1.2)

不变的:struct 语法、./->、初始化方式、对齐的"成因";要变的:对齐/布局/字节序的具体数值——跨平台代码永远不要假设它们。

内容来源映射

内容部分资料位置标记说明
结构体定义/初始化/访问、结构体数组与指针、传值传址、位段语法第一阶段讲义21.x 节来源正文在原资料基础上重新组织表述,未大段复制原文
位段"int 最多 31 位预留符号位"表述第一阶段讲义21.x 节(PAGE 317)纠错见勘误 err-7.3-13:宽度上限与布局均为实现定义
内存对齐规则(offsetof/pragma pack 实测)、柔性数组、可移植性边界(序列化与内存布局)补充原资料对齐仅一句带过;本章补 offsetof 实测、pack 代价与"序列化不能依赖内存布局"的完整讲解,比例约 20%
小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读补充原资料该章无成体系练习,全部新编

本章勘误与更新记录(【纠错】/【更新】)

  • err-7.3-13 · 表述修正 · 出处 PAGE 317

    原文:资料说位段"int 最多 31 位预留符号位"

    正确:位段宽度上限是实现定义的,标准不规定"最多 31 位";可移植代码使用 unsigned int 位段且单段宽度不超过 unsigned int 宽度;具体布局(位顺序/跨存储单元/填充)仍是实现定义的

    原因:第一步报告 7.3 第 13 条:把某个实现的限制当成语言规则会误导读者