第十章:结构体与共用体 —— 自定义数据类型
前言欢迎来到第十章到目前为止我们学过的数据类型都是 单一 的int存一个整数char存一个字符double存一个小数数组存一组相同类型的数据但如果我想描述一个 学生 呢学生有姓名字符串、年龄int、成绩float、性别char…… 这些类型不一样但它们都属于同一个对象。用单独的变量太散了。用数组类型不一样。这时候就需要结构体struct了 —— 它可以把不同类型的数据打包在一起形成一个新的自定义类型。而共用体union更有意思 —— 多个变量共用同一块内存同一时间只用一个。本章我们就来学习什么是结构体怎么定义、怎么用结构体数组、结构体指针、结构体嵌套什么是共用体和结构体有什么区别typedef怎么给类型起别名枚举类型enum简介学完本章你就能自己定义复杂的数据类型了编程的表达力会更上一层楼。准备好了吗让我们开始一、为什么需要结构体先来看一个问题存储一个学生的信息。学生有姓名字符串年龄整数性别字符成绩浮点数不用结构体的写法char name[20] 张三; int age 18; char gender M; float score 95.5;这样写有什么问题四个变量是分散的看不出它们是 一伙的如果有 100 个学生就得定义 400 个变量疯了传参的时候要传 4 个参数很麻烦用结构体的写法struct Student { char name[20]; int age; char gender; float score; };把相关的数据打包成一个整体这就是结构体的意义 ——描述一个对象的多个属性。二、结构体的定义和使用2.1 定义结构体类型用struct关键字定义结构体类型struct 结构体名 { 类型1 成员名1; 类型2 成员名2; // ...更多成员 }; // 注意末尾有分号例子定义一个学生结构体struct Student { char name[20]; int age; char gender; float score; }; // ← 这个分号不能忘⚠️新手常错结构体定义末尾的分号很容易忘一定要记得写定义了struct Student之后struct Student就是一个新的类型名和int、char一样可以用来定义变量。2.2 定义结构体变量有了类型就可以定义变量了。方式一先定义类型再定义变量struct Student { char name[20]; int age; char gender; float score; }; struct Student s1, s2; // 定义两个Student类型的变量方式二定义类型的同时定义变量struct Student { char name[20]; int age; char gender; float score; } s1, s2; // 直接在后面写变量名方式三省略结构体名匿名结构体struct { char name[20]; int age; char gender; float score; } s1, s2;这种方式只能用一次因为没有类型名后面没法再定义新变量了一般不推荐。2.3 访问结构体成员定义了结构体变量后怎么访问里面的成员用点运算符.。变量名.成员名例子#include stdio.h #include string.h struct Student { char name[20]; int age; char gender; float score; }; int main() { struct Student s; // 给成员赋值 strcpy(s.name, 张三); // 字符串要用strcpy赋值 s.age 18; s.gender M; s.score 95.5f; // 读取成员 printf(姓名%s\n, s.name); printf(年龄%d\n, s.age); printf(性别%c\n, s.gender); printf(成绩%.1f\n, s.score); return 0; }运行结果姓名张三 年龄18 性别M 成绩95.5⚠️注意字符串成员不能直接用赋值因为数组名不能直接赋值要用strcpy复制。2.4 结构体的初始化结构体变量也可以在定义的时候初始化用大括号按顺序赋值struct Student s {张三, 18, M, 95.5f};按成员定义的顺序依次赋值name→张三age→18gender→Mscore→95.5。也可以指定成员初始化C99 及以后更清晰struct Student s { .name 张三, .age 18, .score 95.5f // 没写的成员默认初始化为0 };三、结构体数组一个结构体变量存一个学生那存 100 个学生怎么办用结构体数组。3.1 定义结构体数组struct Student stu[100]; // 能存100个学生的数组每个元素都是一个struct Student类型的结构体。3.2 访问数组成员stu[0].age 18; // 第0个学生的年龄 stu[1].score 90.0f; // 第1个学生的成绩先下标取元素再用点访问成员。3.3 初始化struct Student stu[3] { {张三, 18, M, 95.5f}, {李四, 19, F, 88.0f}, {王五, 17, M, 92.5f} };3.4 遍历结构体数组#include stdio.h struct Student { char name[20]; int age; float score; }; int main() { struct Student stu[3] { {张三, 18, 95.5f}, {李四, 19, 88.0f}, {王五, 17, 92.5f} }; // 遍历输出 for (int i 0; i 3; i) { printf(姓名%s年龄%d成绩%.1f\n, stu[i].name, stu[i].age, stu[i].score); } return 0; }运行结果姓名张三年龄18成绩95.5 姓名李四年龄19成绩88.0 姓名王五年龄17成绩92.5四、结构体指针结构体也有指针 —— 指向结构体的指针。4.1 定义结构体指针struct Student s; struct Student *p s; // p指向s4.2 通过指针访问成员有两种方式方式一先解引用再用点(*p).age 18;因为.的优先级比*高所以括号不能少。写起来有点麻烦。方式二用箭头运算符 -推荐p-age 18;-叫箭头运算符专门用来通过结构体指针访问成员写起来更方便、更直观。记住普通变量用点.指针用箭头-。例子#include stdio.h #include string.h struct Student { char name[20]; int age; float score; }; int main() { struct Student s; struct Student *p s; // 通过指针赋值 strcpy(p-name, 张三); p-age 18; p-score 95.5f; // 通过指针读取 printf(姓名%s\n, p-name); printf(年龄%d\n, p-age); printf(成绩%.1f\n, p-score); return 0; }4.3 结构体指针做函数参数结构体可以直接传值但如果结构体很大拷贝开销大。这时候传指针更高效。#include stdio.h struct Student { char name[20]; int age; float score; }; // 传结构体指针效率高还能修改原结构体 void print_stu(const struct Student *s) { printf(姓名%s年龄%d成绩%.1f\n, s-name, s-age, s-score); } int main() { struct Student s {张三, 18, 95.5f}; print_stu(s); // 传地址 return 0; }最佳实践结构体作为参数时优先传指针加 const 保护既高效又安全。五、结构体嵌套结构体里面还可以再包含结构体这叫结构体嵌套。比如学生有 出生日期出生日期又是一个结构体年、月、日// 日期结构体 struct Date { int year; int month; int day; }; // 学生结构体里面包含一个Date struct Student { char name[20]; struct Date birthday; // 嵌套的结构体成员 float score; };访问的时候要一层一层用点struct Student s; s.birthday.year 2008; // 出生年 s.birthday.month 8; // 出生月 s.birthday.day 15; // 出生日如果是指针就混合用struct Student *p s; p-birthday.year 2008; // 指针用-里面的结构体用.嵌套可以多层但一般 2-3 层就够了太深了可读性差。六、共用体union6.1 什么是共用体共用体也叫联合体用union关键字定义。语法和结构体几乎一样但内部原理完全不同。结构体每个成员有独立的内存空间同时存在共用体所有成员共用同一块内存同一时间只有一个有效union Data { int i; float f; char c; };这个共用体的大小是多少不是 4419而是取最大的成员的大小—— 这里最大的是 int 和 float都是 4 字节所以整个 union 占 4 字节。三个成员共用这 4 个字节当 int 用的时候这 4 个字节解释成整数当 float 用的时候这 4 个字节解释成浮点数当 char 用的时候只用第 1 个字节改了一个成员其他成员也会受影响 —— 因为它们用的是同一块内存。6.2 共用体的经典用法拆分字节 / 位操作共用体在嵌入式开发中用得非常多最典型的场景就是用不同粒度访问同一块内存—— 既可以整体操作一个整数又可以单独操作它的某一个字节或某几个位。例子 1拆分 16 位整数的高低字节一个 16 位的 short 占 2 个字节。有时候我们需要单独拿到高字节和低字节比如串口通信、协议解析用共用体就很方便#include stdio.h union ShortByte { unsigned short value; // 整体16位 unsigned char byte[2]; // 拆分2个字节 }; int main() { union ShortByte data; data.value 0x1234; // 整体赋值十六进制 0x1234 printf(整体值0x%04X\n, data.value); // 单独看每个字节小端模式下低地址存低字节 printf(byte[0]低字节0x%02X\n, data.byte[0]); // 0x34 printf(byte[1]高字节0x%02X\n, data.byte[1]); // 0x12 // 修改其中一个字节 data.byte[0] 0xAB; printf(修改低字节后整体0x%04X\n, data.value); // 0x12AB return 0; }运行结果小端机器上整体值0x1234 byte[0]低字节0x34 byte[1]高字节0x12 修改低字节后整体0x12AB不用移位、不用掩码直接通过数组下标就能访问每个字节 —— 清晰又高效。例子 2共用体 位域按位操作寄存器嵌入式开发中经常要操作寄存器的某几位。配合位域bit-field和共用体可以同时支持整体赋值和按位访问#include stdio.h // 8位状态寄存器 union StatusReg { unsigned char byte; // 整体访问整个寄存器 // 按位访问每一位对应一个标志位 struct { unsigned char enable : 1; // 第0位使能位 unsigned char ready : 1; // 第1位就绪位 unsigned char error : 1; // 第2位错误位 unsigned char mode : 2; // 第3-4位模式2位 unsigned char : 3; // 第5-7位保留 } bits; }; int main() { union StatusReg reg; // 整体赋值 reg.byte 0x0B; // 二进制 00001011 // 单独读每一位 printf(使能位%d\n, reg.bits.enable); // 1第0位 printf(就绪位%d\n, reg.bits.ready); // 1第1位 printf(错误位%d\n, reg.bits.error); // 0第2位 printf(模式 %d\n, reg.bits.mode); // 2第3-4位 10 // 单独修改某一位 reg.bits.error 1; // 把错误位置1 printf(置错误位后整体0x%02X\n, reg.byte); // 0x0F00001111 return 0; }运行结果使能位1 就绪位1 错误位0 模式 2 置错误位后整体0x0F用位域操作寄存器位比手写移位掩码(reg 2) 1可读性好太多了 —— 直接看名字就知道这一位是干嘛的。这也是共用体在嵌入式里最经典的用法之一。6.3 共用体的其他应用场景场景一节省内存同一个位置有时候存整数有时候存浮点数不同时用 —— 用共用体可以省空间。在内存紧张的嵌入式设备上很有价值。场景二变体类型一个数据可能是多种类型中的一种配合一个标记字段使用struct Variant { int type; // 标记当前是什么类型 union { int i; float f; char s[20]; } data; };简单记结构体是 同时有多个共用体是 同一时间只有一个、但可以多种方式解读同一块内存。大部分时候用结构体需要字节 / 位级别的精细操作、或者省内存时才用共用体。场景三变体类型一个数据可能是多种类型中的一种配合一个标记字段使用struct Variant { int type; // 标记当前是什么类型 union { int i; float f; char s[20]; } data; };简单记结构体是 同时有多个共用体是 同一时间只有一个。大部分时候用结构体只有特殊场景才用共用体。七、枚举类型 enum了解7.1 什么是枚举枚举就是把所有可能的值一一列出来变量的值只能是其中之一。用enum关键字定义enum Weekday { MON, // 星期一 TUE, // 星期二 WED, THU, FRI, SAT, SUN // 星期日 };定义之后enum Weekday就是一个类型变量的值只能是这 7 个中的一个。7.2 枚举的本质枚举本质上就是整数默认从 0 开始依次递增MON 0TUE 1WED 2...SUN 6也可以手动指定值enum Weekday { MON 1, // 从1开始 TUE, // 2 WED, // 3 // ... };7.3 枚举的使用#include stdio.h enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN }; int main() { enum Weekday day FRI; if (day SAT || day SUN) { printf(周末休息\n); } else { printf(工作日上班\n); } return 0; }枚举的好处是可读性好。用MON、TUE比直接用 0、1 清晰多了。八、typedef 类型重定义8.1 什么是 typedeftypedef可以给已有的类型起一个别名新名字。语法typedef 原类型名 新类型名;8.2 为什么要用 typedef原因一简化长类型名结构体类型写起来太长了struct Student。用 typedef 简化typedef struct Student { char name[20]; int age; float score; } Student;之后就可以直接用Student定义变量不用写struct了Student s1; // 等价于 struct Student s1;这是 C 语言里最常见的 typedef 用法。原因二提高可移植性比如int在不同平台大小可能不一样。如果你写typedef int my_int;以后想改成long只需要改 typedef 这一行所有用my_int的地方自动就变了。原因三提高可读性给复杂的类型起个有意义的名字代码更清晰。8.3 常见用法给结构体起别名typedef struct { char name[20]; int age; } Student;给指针类型起别名typedef int* IntPtr; IntPtr p1, p2; // p1和p2都是int*给函数指针起别名typedef int (*FuncPtr)(int, int); FuncPtr p; // p是函数指针注意typedef 是定义类型别名不是定义新类型。它只是给现有类型起了个外号本质上还是原来的类型。九、综合示例学了这么多我们来看一个综合的例子 —— 学生管理系统的简化版#include stdio.h #include string.h // 用typedef定义结构体类型 typedef struct { char name[20]; int age; float score; } Student; // 打印学生信息传指针高效 void print_student(const Student *s) { printf(姓名%s年龄%d成绩%.1f\n, s-name, s-age, s-score); } // 找成绩最高的学生返回指针 Student *find_top(Student *stu, int n) { Student *top stu[0]; for (int i 1; i n; i) { if (stu[i].score top-score) { top stu[i]; } } return top; } int main() { Student stu[3] { {张三, 18, 95.5f}, {李四, 19, 88.0f}, {王五, 17, 92.5f} }; printf(所有学生\n); for (int i 0; i 3; i) { print_student(stu[i]); } Student *top find_top(stu, 3); printf(\n成绩最高的是%s%.1f分\n, top-name, top-score); return 0; }运行结果所有学生 姓名张三年龄18成绩95.5 姓名李四年龄19成绩88.0 姓名王五年龄17成绩92.5 成绩最高的是张三95.5分十、本章小结本章我们学习了 C 语言中的自定义数据类型1. 结构体 struct把不同类型的成员打包在一起定义struct Name { ... };末尾分号不能忘访问成员变量用.指针用-可以有结构体数组、结构体指针、结构体嵌套2. 共用体 union所有成员共用同一块内存同一时间只有一个有效大小 最大成员的大小特殊场景使用节省内存、查看二进制、变体类型3. 枚举 enum把可能的值一一列举出来本质是整数默认从 0 开始优点可读性好4. typedef给类型起别名最常用简化结构体类型名不用每次写 struct提高可移植性和可读性学习建议结构体是 C 语言进阶的必备知识一定要多练。建议你写一个小型的学生管理系统 / 通讯录程序用结构体数组存储数据实现增删改查 —— 写完你对结构体的理解就通透了。十一、写在最后Programs must be written for people to read, and only incidentally for machines to execute.——Harold Abelson哈罗德・埃布尔森MIT 计算机教授翻译成中文就是程序是写给人读的只是顺便能在机器上运行。结构体、枚举、typedef…… 这些语法的意义是什么不仅仅是让代码能跑更是让代码更清晰、更易读。好的代码别人看一眼就懂你在表达什么。写代码和写文章一样 —— 不仅要对还要美。清晰的结构、恰当的命名、合理的抽象都是代码之美的一部分。⚠️ 关于本文由于笔者水平有限文中难免存在疏漏、错误或表述不够准确的地方。如果你在阅读过程中发现了任何问题 —— 无论是概念错误、代码 bug、还是表述不清 —— 都欢迎在评论区留言指正我会及时修正。技术之路道阻且长行则将至。我们互相学习共同进步