恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C语言构造数据类型详解:结构体、共用体、枚举与typedef
首页
资讯中心
/
C语言构造数据类型详解:结构体、共用体、枚举与typedef
C语言构造数据类型详解:结构体、共用体、枚举与typedef
发布时间:2026/10/20 9:31:19
我最早写课设时被一个非常简单的问题卡了很久一个学生有学号、姓名、三门课成绩我到底该用几个变量去装一个学生还好说名字数组、学号数组、成绩数组各开一份勉强能跑但一旦学生数量变成十个、一百个数组写法就开始变得僵硬成绩和姓名对不上号排序、查找、传参全都难受。后来我终于想明白这是C语言里“构造数据类型”该解决的问题C语言除了基本类型还支持用基本类型搭出更复杂的数据组织方式最常见的有结构体、共用体、枚举以及总被我们和结构体一起用的typedef。如果你刚学完指针和数组正准备开始写第一个像样的项目或者在做计算机基础类的笔试面试复习这篇关于C语言其他构造数据类型的系列第一篇可以帮你把这些类型一次性理清楚。这篇文章我会从“为什么需要它们”讲起然后依次拆解结构体、共用体、枚举、typedef这四个类型的定义、内存布局、初始化方式和典型坑点最后用一个综合配置系统的例子把四种类型串起来。我会尽量把这些知识还原到真实项目里不是背概念而是告诉你写代码时该怎么选、踩过什么坑。1. 一个变量装不下一个学生构造数据类型要解决什么问题1.1 从基本类型到组合类型C语言的基本数据类型char、int、float、double本质上是若干种“原子”。原子很好理解单个数字、单个字符一个变量正好装下。但现实世界里的实体通常有多个属性比如一部手机有型号、价格、库存一个网络数据包有长度、序号、内容。这时候基本类型就不够用了。数组解决了一部分问题同类型的数据可以沿着下标铺开。十个学生的姓名可以开一个二维字符数组。但学生的学号和姓名不是同一类型成绩数组和姓名数组又是分离的靠下标对齐去维护它们很痛苦。这种“把不同类型字段绑定在一起管理”的需求就是结构体出现的根本动机。共用体和枚举解决的则是另外两类问题。共用体解决“同一块内存需要在不同时刻被不同类型解读”的需求比如协议解析时一段数据可能是整数也可能是四个字节。枚举解决的是“大量魔法数字阅读起来不明所以”的需求比如用0表示待机、1表示运行、2表示故障不如直接写RUNNING。1.2 四种“其他构造类型”的定位在C语言课程里构造数据类型通常被分成几类数组、结构体、共用体、枚举。数组经常被单独拉出来讲因为它是同类型元素的线性集合使用方式非常特殊。今天这一篇作为系列第一篇先聚焦在结构体、共用体、枚举上再加上一个严格来说“不算构造类型”却从来离不开的typedef。我把四者的定位总结成一句话结构体帮我们把一堆不同类型的数据打包成一个整体。共用体帮我们在一块内存上做多种解释。枚举帮我们把离散的取值变得有名字、有语义。typedef帮我们给类型起别名让复杂的类型声明可读。这句话很朴素但真到选型的时候很多新手会在这四者之间犹豫。比如有人会用枚举来表示颜色组合用结构体存只偶尔出现的可选字段这些用法不算错但未必是最合适的选择。后面每一章我会把边界讲清楚。1.3 为什么把结构和类型声明分开看还有一点值得先说明结构体和共用体描述的是“内存布局”枚举描述的是“取值范围”它们本质上是两个维度的概念。你可以有一个枚举类型的结构体成员也可以有一个共用体类型的结构体成员。理解了这一层后面看综合案例的时候就不会晕。我在教学和实际编码中最大的体会是不要只把结构体当作“多个变量的打包”而要把它当作“一个领域对象的建模工具”。你把学生、订单、设备都建模成结构体代码的意图立刻就清楚了。这也是构造数据类型比基本类型更有生产价值的原因。2. 结构体最常用的聚合类型2.1 定义与初始化别把typedef struct写成struct结构体的定义方式大概是C语言里被写错次数最多的语法之一。最基本的形式是这样的struct student { char id[10]; char name[32]; float scores[3]; };注意这个分号结构体定义结束的分号绝对不能丢。丢了分号编译错误千奇百怪而且编译器报错的行号经常和实际错误位置差很远。我见过不少新手被这个分号折磨第一次碰到时直接懵了。定义之后声明变量有三种写法struct student stu1; struct student stu2;这在C语言里完全合法。但几乎所有项目里大家都会写第四种用typedef给这个结构体起一个简短名字。typedef struct student { char id[10]; char name[32]; float scores[3]; } Student;之后就可以直接写Student stu1;不需要每次带struct。这样写没有性能差异纯粹是代码风格的改善。但有一个我非常想强调的坑如果你用typedef且结构体内部需要引用自己比如链表节点、树节点就不能用匿名struct偷懒。typedef struct node { int data; struct node *next; // 这里必须写 struct node不能写 Node * } Node;在结构体声明的内部Node还没有定义完所以只能写struct node *next。这种错误在链表的初学者代码里出现频率极高。结构体变量的初始化也有讲究。最直接的是声明时按成员顺序初始化Student stu1 {2025001, zhang, {88.5, 90.0, 76.5}};C99之后还支持指定初始化器可以按成员名赋值不用记得顺序Student stu2 { .id 2025002, .name li, .scores {80.0, 85.0, 92.0} };这段代码在GCC和Clang下都很常见但如果你在跑一些比较老的嵌入式编译器或者项目要求C89兼容就得改成传统顺序初始化。我踩过的坑是在C99模式下用了指定初始化器后来移植到另一套工具链时编译器不支持结果花了一个下午把所有初始化重新排了一遍。2.2 嵌套、数组与指针成员几个绕弯子的细节结构体里可以装数组比如char name[32]结构体里也可以有另一个结构体比如地址字段typedef struct address { char city[32]; char street[64]; int zip; } Address; typedef struct person { char name[32]; Address addr; } Person;访问嵌套成员用的还是点运算符p.addr.zip。这里容易出错的地方是如果addr本身是一个指针比如Address *addr那访问就变成p.addr-zip。到底是点还是箭头核心规则就一条左侧是结构体变量就用点左侧是结构体指针就用箭头。说到指针成员有一个经验性的警告结构体里最常见的问题不是访问而是复制。C语言允许直接写Person p2 p1;这叫结构体整体赋值。如果结构体里只有普通数值和数组这个赋值没问题。但如果结构体里有指针成员直接赋值只是把指针值复制了过去两个变量的指针指向同一块内存。这时候如果其中一个函数负责释放另一个还在使用程序崩溃就离你不远了。typedef struct buffer { int len; char *data; } Buffer; Buffer b1; Buffer b2 b1; // b2.data 和 b1.data 是同一个地址数据共享在某些场景下是设计意图但大多数时候你是想深拷贝。正确的做法是自己写一个复制函数对新结构体单独申请内存把数据内容拷过去而不是只复制指针。这个道理和Java里的对象引用复制、深拷贝浅拷贝是相通的。另一个容易绕弯的地方是结构体数组。声明Student students[100];等于一次性定义了100个学生每个学生都是完整结构体。初始化的时候可以这样写Student all_students[2] { {2025001, zhang, {88.5, 90.0}}, {2025002, li, {80.0, 85.0}} };结构体数组配合循环是处理批量数据最基本的手段。我常对学生说结构体数组就是“表格”内存中的样子一行一个结构体每列一个字段。2.3 内存对齐结构体不是字段的简单拼接这是结构体最重要的实战知识点也是刚开始学C的人最不会去想的点。很多人以为定义了一个char int char的结构体大小应该是1 4 1 6字节。实际上一测很可能是12字节。#include stdio.h struct sample { char c1; int i; char c2; }; int main(void) { printf(%llu\n, (unsigned long long)sizeof(struct sample)); return 0; }在常见的32位平台上这个值是12。解释起来并不复杂硬件对齐规则通常要求int类型变量的地址必须是4的倍数所以c1后面被塞了3个填充字节让i落在对齐的位置i之后c2占1字节结构体总长度还要是最大对齐数的整数倍于是末尾又补了3字节。可别小看这个填充行为。它带来两个直接影响。第一网络通信、文件存储中直接fwrite一个结构体底层文件里会混入这些填充字节换一台机器、换一个编译器写入的数据布局可能就不一样了。第二结构体字段顺序会影响内存浪费。把上面的声明改成struct sample { int i; char c1; char c2; };大小就变成8字节而不是12字节。在嵌入式项目中一个结构体如果频繁被分配省下这些填充字节是非常有价值的。但如果你的场景就是跨平台通信更稳妥的办法是不依赖对齐逐字段写文件或按字节数组拼。也可以使用__attribute__((packed))或者#pragma pack(1)来关掉填充。我的建议是在确实需要内存紧凑、且你能接受访问未对齐成员可能产生性能损失或移植风险的前提下再去用packed否则优先调整字段顺序让编译器自然对齐既安全又省心。我在排查一个嵌入式设备通信问题时就因为结构体对齐规则发现同样一份结构体数据在PC端解出来的字段有偏移错位。排查过程从“数据哪去了”到“填充字节”最后用offsetof宏打印每个字段偏移量才定位。后来我就养成了习惯凡是涉及结构体跨平台传输要么用明确的序列化函数要么在代码里加static_assert验证字段偏移。这是用血泪换来的经验。3. 共用体同一块内存的不同解释3.1 联合体与结构体的本质区别共用体在C语言里用union关键字定义形态上和结构体极像union value { int i; float f; char bytes[4]; };区别在于内存分配方式。结构体里每个成员独立占用存储空间大家共存共用体里所有成员共享同一段内存起点存储空间大小只要够装下最大的成员就行。上面这个value在常见的32位平台上大小是4字节不管放int还是float都是4字节。理解共用体的最好方式是把它当成“一块内存多个视图”。同一块4字节内存你可以把它当作整数用也可以当作浮点数用也可以当作四个字节数组看待。CPU不管你把它解释成什么具体怎么解释取决于你访问的是哪个成员。结构体和共用体经常会让人混淆我列过一张对比表维度structunion成员存储每个成员独立存储所有成员共享存储区占空间各成员空间之和加上对齐填充最大成员的大小成员修改互不影响可全部有意义修改一个成员会覆盖其他成员典型用途建模实体对象表达变体数据、解析底层格式这张表可以当作第一轮筛选依据你需要的对象同时具备多个属性选struct某一个值在不同场景下类型不同、同一时刻只需要一种解释选union。3.2 大小端判断与寄存器位域解析共用体最常见的经典题目就是判断大小端。在PC这样的常见平台上写整数1内存里如果是最低字节为1后面全是0那是小端反过来把1存在最高字节是大端。#include stdio.h union endian_check { int i; char c; }; int main(void) { union endian_check u; u.i 1; if (u.c 1) { printf(little endian\n); } else { printf(big endian\n); } return 0; }原理不复杂union的成员共享地址往u.i写入1之后从u.c视角去读同一块内存的最低地址字节读到1就是小端。这段代码在笔试里出现频率很高在真实的通信协议调试里也很有用。共用体配合位域是嵌入式领域非常实用的组合。很多外设寄存器的32位数据里每几个bit表达一个含义。你可以用位域结构体去描述它再和整个寄存器数值做一个联合union uart_status { uint32_t raw; struct { uint32_t tx_empty : 1; uint32_t rx_full : 1; uint32_t error : 6; uint32_t reserved : 24; } bits; };这段代码的好处是想整体判断状态时读u.raw想单独看某个标志位时读u.bits.tx_empty。不用手写一堆掩码和移位操作。但我要提醒一句位域的分配方向低位从哪个bit开始是由编译器实现的不是C标准统一的。这段代码移植到不同编译器时必须先确认位域顺序。我自己遇到过在GCC下正常的寄存器解析换到另一套交叉编译器之后bits字段顺序反了调了半天才发现是位域顺序不同。3.3 共用体的踩坑点覆盖与类型混淆使用共用体时最经典的问题是覆盖union里所有成员共享内存所以你先后给两个成员赋值最后只有最后一次赋值能稳定读取。union value v; v.i 100; v.f 3.14; // 此时 v.i 的值已经不具备确定性这一点在工作多年的开发者那里也会出错。修改一个成员不代表“另一个成员被破坏了”而是“这块内存被重复使用了”。所以在使用共用体前必须有一种办法知道当前应该按哪个成员读。通常的做法是加一个类型标志字段这就是所谓的“带标签的联合体”。enum value_type { TYPE_INT, TYPE_FLOAT }; struct tagged_value { enum value_type tag; union { int i; float f; } value; };读的时候先看tag是哪个类型再访问对应的union成员。这种写法是很多高级语言里“可选类型”“变体类型”的C语言实现雏形。另一个坑是隐式的类型转换。你写了一个int成员然后不小心把union赋值给另一个包含指针成员的结构里的变量编译器不一定警告运行时数据解释却完全不同。所以我建议在封装共用体访问时统一用带tag的包装而不是直接暴露union给所有调用方。需要额外说明的是按C标准如果一个共用体成员被写入之后直接读取另一个成员在严格的标准视角下属于实现定义行为具体表现因编译器而异。虽然GCC、Clang的实际表现基本就是“按相同内存的字节重新解释”但写跨平台代码时不应假设这是完全可移植的。能通过tag保证“读的一定是最后一次写过的成员”才是稳妥做法。4. 枚举让魔法数字变成有名字的常量4.1 枚举的取值规则与底层类型枚举定义起来大概是C语言里最轻松的类型enum weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY };默认情况下第一个枚举常量从0开始后面的依次加1。也就是说MONDAY等于0TUESDAY等于1依此类推。你也可以手动指定值enum status_code { OK 0, NOT_FOUND 404, SERVER_ERROR 500 };指定值的枚举非常适合表达HTTP状态码、错误码、协议编号这些有现实含义的枚举集合。关于枚举的底层类型在C23之前通常是由编译器按能容下所有枚举值的整型类型来决定绝大多数实现里就是int或者unsigned int。枚举常量本身是整型常量可以进行加减赋值比较但不能在编译期当数组维度直接使用这一点和#define不同更和C里的enum class不同。C语言枚举的类型检查比较弱把一个整数赋给枚举变量编译器通常只警告不报错。我在一段代码里特别喜欢手动调整枚举值后来发现这样会带来一个很隐蔽的问题如果中间某几个值跳号了比如BIT11, BIT22, BIT44你用for循环从第一个枚举迭代到最后一个枚举会漏掉跳过的值。别问我是怎么发现的调整芯片GPIO模式映射时调试器的值列表突然少了一项查了半天才发现自己写了个跳号枚举。4.2 枚举在状态机和配置项里的用法枚举在真实项目里最大的价值是替代魔法数字。比如一个程序有三种状态空闲、忙、出错。你当然可以写int state 0;但你不得不时刻记住0代表什么。更好的写法是typedef enum { STATE_IDLE, STATE_BUSY, STATE_ERROR } DeviceState; DeviceState state STATE_IDLE;这样写有几个好处。第一代码阅读者不需要去查注释名字本身就说明了含义。第二IDE和调试器可以直接显示枚举名不用手动对照数字。第三incomplete切换状态时用赋值枚举比直接写整数更不容易乱。状态机是我不厌其烦推荐枚举的场景。一个最基础的设备状态机可以这样建模typedef enum { ST_IDLE, ST_INIT, ST_RUNNING, ST_STOPPING } State; State next_state(State current, int event) { switch (current) { case ST_IDLE: return (event 1) ? ST_INIT : ST_IDLE; case ST_INIT: return ST_RUNNING; case ST_RUNNING: return (event 2) ? ST_STOPPING : ST_RUNNING; case ST_STOPPING: return ST_IDLE; default: return ST_IDLE; } }配合switch语句每个case直接用一个枚举名代码的可读性远远超过用0、1、2、3。这里我还想强调一点switch的default分支不能省。即使你认为状态已经穷尽未来的维护者可能给枚举增加新值default兜底会让程序不至于因为漏分支而进入未知行为。4.3 别拿枚举当安全检查很多人刚学会枚举之后容易把它当成一种“类型安全”机制既然定义了枚举类型那这个变量就只能取这些值了吧这种想法在C语言里是危险的。C语言的枚举不是强类型你完全可以把一个整数强转成枚举typedef enum { RED, GREEN, BLUE } Color; Color c (Color)999;编译器在开启警告时有概率提醒但绝不保证报错。运行时c的值就是999不会因为“不在枚举范围内”自动变成其他值。C语言编辑器非常多但都不如C的enum class那么严格。所以我的建议是把枚举当成一种“代码语义组织工具”不要依赖它做数据校验。如果从外部输入里读取了一个整数并赋值给枚举读完之后必须手动校验范围否则后续逻辑可能产生意大利面条一样的错误分支。我在一个模拟器项目里吃过这个亏外部配置项给了个越界模式值程序没有立刻崩溃而是进入了完全未预料的路径最后靠加日志才找到根因。另外一个值得说的细节枚举常量是全局可见的标识符同一个作用域内不能重名。不同枚举类型里如果都定义了OK会直接碰撞报错。所以在大型项目里很多人会给枚举常量加前缀比如COLOR_RED、STATE_IDLE就是为了避免命名冲突。这个习惯越早培养越好。5. typedef给类型取个好记的名字5.1 typedef不是定义新类型typedef不是构造数据类型它是给已有类型起别名。它的语法位置很有意思只要在一个声明前面加上typedef原来的“变量声明”就变成了“类型别名声明”。typedef unsigned int uint32;这一行定义了uint32作为unsigned int的别名。注意它没有创建新类型uint32和unsigned int完全可以互换使用。这一点和#define有本质区别#define是文本替换发生在编译预处理阶段不做类型检查typedef是编译期处理要遵守声明规则。在指针这里两者差异尤其明显#define PTR_INT int * typedef int * INT_PTR; PTR_INT a, b; // 等价于 int *a, b; 其中 b 是 int不是指针 INT_PTR c, d; // c 和 d 都是 int *新手经常在这里被绕进去。宏替换是逐字替换的PTR_INT a, b展开后变成int * a, b只有a是指针typedef则不一样它把int *整体定义为INT_PTR因此c和d两个变量都是指针。面试笔试如果问“typedef和#define的区别”几乎必出这道题。5.2 函数指针与数组的typedef体验typedef最让人头皮发麻的用法是函数指针。C语言的函数指针声明本来就难读比如int (*handler)(int, void *);这是个指向“参数为int和void*返回int”的函数的指针。如果项目里要定义好几个同类型的函数指针每次都写一遍这种语法简直像念咒语。用typedef包一层之后代码会脱胎换骨typedef int (*Handler)(int, void *); Handler h1; Handler h2;定义两个处理函数之后注册回调就非常自然int process_a(int id, void *ctx); int process_b(int id, void *ctx); Handler current process_a; current process_b;函数指针的typedef是把“从一个复杂声明到一个好读的名字”的体验拉到极致。在不少项目里回调机制、注册表、接口层核心就是靠typedef函数指针组织起来的。如果不会读函数指针声明可以遵循“向右看再向左看”的原则先找标识符看它先和谁结合。int (*handler)(int, void *)里*表明handler是个指针右边圆括号表明这个指针指向一个函数函数参数是int和void*返回int。typedef还能给数组起别名typedef char Name[32]; Name n1; Name n2;这比写char n1[32]; char n2[32];要直观一些。但说实话我自己很少给数组起别名这种用法因为数组本身退化成指针的规则太容易让人误会。这种能力了解一下即可。5.3 项目里的命名规范和可读性收益typedef最常见的实战场景还是和struct、enum搭配typedef struct sensor_data { float temperature; float humidity; } SensorData; typedef enum { MODE_AUTO, MODE_MANUAL } ControlMode;这样做的好处是不用写struct sensor_data data;只需要SensorData data;。代码简洁而且从类型上就能看出它是数据载体还是模式枚举。不过我也要劝一句typedef并不是用得越多越好。如果类型只在某个函数里用一次加typedef反而增加了不必要的间接层。另外给结构体起别名时我习惯保留原本的struct标签名像上面struct sensor_data和SensorData都存在因为某些场景比如结构体内部的自我指涉还需要struct sensor_data这个名字。匿名struct加typedef写起来省事但在需要反射或文档生成的场景里会丢失关键信息。命名后缀也是个值得统一的事。很多开源库喜欢用_t后缀表示类型比如size_t、uint32_t。这套做法的可辨识度极高但POSIX标准保留了_t后缀如果你的项目要跑在严格POSIX环境里最好在自家自定义类型上避免_t后缀或者至少先查清楚不冲突。项目内部的类型命名我偏好SensorData、ControlMode这种驼峰。一个项目里类型命名风格必须一致否则typedef带来的可读性优势会被风格混乱抵消。6. 综合案例用四类类型搭一个迷你配置系统6.1 结构体枚举typedef的组合用法把这四种类型用在同一个例子里看它们怎么协作。假设我们要做一个简单的传感器节点配置模块配置项包含设备ID、工作模式、重试次数、启动标志。设备工作模式是固定几个值所以用enum所有配置打包成一个整体用struct配置的来源可能是一段原始字节也可能是结构化字段为了解析方便可以加一个union。#include stdio.h #include stdint.h typedef enum { MODE_AUTO, MODE_MANUAL, MODE_SLEEP } WorkMode; typedef struct config { uint32_t device_id; WorkMode mode; uint8_t retry_count; union { uint32_t raw_flags; struct { uint32_t enable_log : 1; uint32_t enable_alert : 1; uint32_t reserved : 30; } bits; } flags; } Config; static void print_config(const Config *cfg) { printf(id%u mode%d retry%u log%d alert%d\n, cfg-device_id, (int)cfg-mode, cfg-retry_count, cfg-flags.bits.enable_log, cfg-flags.bits.enable_alert); } int main(void) { Config cfg { .device_id 1001, .mode MODE_AUTO, .retry_count 3, .flags.raw_flags 0 }; cfg.flags.bits.enable_log 1; cfg.flags.bits.enable_alert 1; print_config(cfg); return 0; }这里WorkMode枚举让配置里的模式有了语义Config结构体聚合了所有字段union包住raw_flags和位域结构体让两个视图可以互相转换typedef给每个类型都起了简短名称。实际项目中你还会把配置模块封装成几个函数比如初始化默认配置、校验配置合法性、保存配置到Flash但核心数据结构就是这样组织的。这段代码的意图也很清楚读取配置时可以先拿到一个整体的raw_flags需要判断某一位时直接看bits。这和前面的寄存器解析是同一套路。6.2 项目实战中的初始化与复用心得综合案例里最容易被忽略的是初始化和复用。我自己的习惯是每个结构体类型都写一个xxx_init或xxx_default函数返回一个默认填充好的实例。原因是C语言不会自动清零局部结构体Config cfg;里面是随机值。如果忘记初始化就开始读取轻则行为诡异重则引发数组越界。两种常见的初始化方式都可以接受方式一用指定初始化器Config cfg {0};这一个{0}会把整个结构体清零简单、安全。之后需要改哪个字段再逐个赋值。方式二单独写初始化函数static Config default_config(void) { Config cfg {0}; cfg.device_id 0; cfg.mode MODE_AUTO; cfg.retry_count 2; return cfg; }返回结构体在C99之后完全没问题编译器通常能优化得很干净不要被“返回整个结构体很浪费”这种旧观念吓到。如果项目要求严格C89再考虑传指针的方式。结构体复用还有一个容易踩的坑是如果结构体里的指针成员是指向外部传入的buffer{0}清零之后你必须手动重新赋值指针否则解引用空指针会崩。换句话说{0}能清空结构体但清空之后字段值需要按业务重新构建。一个结构体在有指针成员和没有指针成员时初始化工作的劳动强度完全不同。我在写配置模块时会额外给每个字段写校验函数确保外部传入的mode在枚举范围内retry_count不超过最大重试次数。枚举不是安全边界校验才是。最后分享一个我个人的编码习惯凡是结构体作为函数参数传递我几乎一律使用指针而不是拷贝整个结构体。大部分结构体的体积远大于一个指针按值传参会多一次完整复制更重要的是按指针传函数内部如果不需要修改加一个const限定的指针对调用方意图非常清楚。对于返回值如果结构体特别大我倾向让调用方传入一个指针由函数填充内容而不是返回临时结构体。当然这只是风格不是标准答案但可以避免很多不必要的拷贝和不经意间的数据不一致。现在每次拿到一个新的设备协议我做的第一件事几乎都一样把协议里的消息体用struct建模把字段里的枚举状态用enum整理把变长或者多种表示的部分用union处理再用typedef把类型命名变得好读。这一套组合用熟了之后再看那些五花八门的配置文件、通信协议、寄存器手册心里会踏实很多。C语言的构造数据类型不算多但每一个都对应着一类真实问题。先把结构体和枚举用顺手union在嵌入式场景里多多练习typedef在每一次项目里自然建立后面的指针进阶和更复杂的系统设计都会顺畅不少。