C语言基础回顾(2026/7/19)

📅 2026/7/18 20:55:02
C语言基础回顾(2026/7/19)
目录复合数据类型1结构体①什么是结构体②定义结构体变量的两种方式③访问结构体成员④结构体初始化⑤结构体数组⑥结构体指针传参⑦用 typedef 简化结构体类型名Struct⑧struct的四种写法12342枚举enum①什么是枚举②如何定义枚举③声明 初始化④遍历⑤用作状态机切换3共用体union①什么是共用体②共用体和结构体的区别③union怎么创建④union干什么用的1拆字节2寄存器3协议解析4typedef① 给结构体起别名② 给函数指针起名字③ 给复杂类型起名字例如数组④ 给基本类型起别名⑤ 给枚举起别名⑥ 给 union 起别名总结复合数据类型1结构体①什么是结构体现实生活中一个“学生”有学号整型、姓名字符串、成绩浮点型。如果用单独的变量存会很散乱。结构体就能把它们组合在一起struct Student { int id; // 学号 char name[20]; // 姓名 float score; // 成绩 };struct Student 就是一个新的类型名类似 int、char但它里面包含了多个成员。②定义结构体变量的两种方式方式一先定义类型再声明变量struct Student { int id; char name[20]; float score; };struct Student stu1, stu2; // 声明两个结构体变量成分解析1这里的struct 其实就是很变量int a 的int 作用是一样的2这里的Student叫做结构体标签相当于这个结构体的类型比如DogCatBook有点像python中的类3花括号里面是什么{ int id; char name[20]; float score; }这里叫成员/成员变量它们就是 Student 里面的数据。4第四部分struct Student stu1, stu2;这一句才是真正创建变量。相当于创建两个 Student 类型变量名字分别叫stu1stu2就像int ab 这里只不过是创建两个 struct类型 变量在内存中方式二定义类型的同时声明变量不推荐常用因为类型名可复用性差struct Student { int id; char name[20]; float score; } stu1, stu2;③访问结构体成员使用点运算符.#include stdio.h #include string.h int main() { struct Struct { int id; char name[20]; float score; }; struct Struct stu1; stu1.id 1001; strcpy(stu1.name, 张三); stu1.score 92.5; printf(学号%d, 姓名%s, 成绩%.1f, stu1.id, stu1.name, stu1.score); return 0; }④结构体初始化可以在声明时直接初始化顺序要和成员定义一致struct Student stu2 {1002, 李四, 88.0};也可以只初始化部分成员C99 以后支持指定初始化struct Student stu3 {.id 1003, .score 95.0}; // name 会是空字符串⑤结构体数组结构体数组就是个数组只不过每个元素不是一个 int 或 float而是一个结构体。你熟悉的普通数组int scores_arr[3]; // 3 个 int型数组成员 scores_arr[0] 90; scores_arr[1] 85; scores_arr[2] 92;结构体数组也一样把int换成你定义的结构体类型struct Student { int id; char name[20]; float score; }; struct Student stus[3]; // 含有3 个 struct Student 变量前边的例子是3个int 变量 // 声明一个结构体数组存 3 个学生 struct Student stus[3]; // 赋值——和单个结构体一模一样只是多了 [下标] stus[0].id 1001; strcpy(stus[0].name, 张三); stus[0].score 92.5; stus[1].id 1002; strcpy(stus[1].name, 李四); stus[1].score 88.0; stus[2].id 1003; strcpy(stus[2].name, 王五); stus[2].score 95.0; // 遍历打印 for (int i 0; i 3; i) { printf(学号%d, 姓名%s, 成绩%.1f\n, stus[i].id, stus[i].name, stus[i].score); } return 0; }嵌入式里用得不少哦嵌入式系统本质上是在管理一组设备或一组数据点天然适合用结构体数组来表达 1. 传感器/设备管理表 typedef struct { uint8_t addr; // I2C/SPI 地址 char name[12]; // 传感器名称 float offset; // 校准偏移 float scale; // 量程系数 void (*init)(void); // 初始化函数指针 } Sensor; Sensor sensors[] { {0x76, BMP280, 0.0, 1.0, bmp280_init}, {0x68, MPU6050, 0.5, 2.0, mpu6050_init}, {0x40, SHT30, -0.3, 1.0, sht30_init}, };⑥结构体指针传参结构体可以直接传给函数但默认是值传递仍旧会拷贝一份void printStudent(struct Student s) { printf(%d %s %.1f\n, s.id, s.name, s.score); } int main() { struct Student stu {1, 小明, 89.5}; printStudent(stu); return 0; }调用函数printStudent(stu);很多人以为形参 s指向 stu实际上不是实际上发生的是复制整个结构体在 STM32 这种 RAM 本来就很紧张、CPU 性能也有限的平台这种额外复制尤其应该避免。因此不要传递整个结构体而是传地址。#include stdio.h #include string.h struct Student { int id; char name[20]; float score; }; void printStudent(struct Student *p) { printf(%d %s %.1f\n, p-id, p-name, p-score); } int main() { struct Student stu {1, 小明, 89.5}; printStudent(stu); return 0; }函数里面没有复制整个结构体p只是保存了stu的地址p-score 100。等价于stu.score 100因为它们访问的是同一块内存。⑦用 typedef 简化结构体类型名Structtypedef在 C/C 中给结构体起别名省去每次写struct关键字的麻烦。每次写struct Student有点长可以用typedef起别名typedef不创建新类型它只是给已有类型取一个新名字给结构体改名是用法最普通的一种typedef struct { int x; int y; } Point; // Point 现在就是类型名 Point p1; // 不用再写 struct p1.x 10; p1.y 20;#include stdio.h #include string.h typedef struct { int id; char name[20]; float score; } Student; void printStudent(Student *p) { printf(%d %s %.1f\n, p-id, p-name, p-score); } int main() { Student stu {1, 小明, 89.5}; printStudent(stu); return 0; }⑧struct的四种写法1先定义结构体类型再定义变量最规范struct Student { int id; char name[20]; }; struct Student stu1; // 定义变量时必须带 struct struct Student stu2;类型名struct Student注意Student本身不是类型名必须配合struct使用。特点类型与变量分离清晰且易于复用。用途最常用的写法适合需要多次定义该结构体变量的场景。2定义类型的同时定义变量一次性struct Student { int id; char name[20]; } stu1, stu2; // 这里直接定义了两个变量 // 后续仍可使用该类型 struct Student stu3;类型名struct Student。特点在声明结构体末尾的}后直接跟变量列表变量stu1、stu2立即分配内存。用途当某个类型只在局部使用或者想强调这些变量的初始存在时使用。3匿名结构体无标签直接定义变量struct { int id; char name[20]; } stu1, stu2; // 只有这两个变量 // 错误无法再定义同类型变量因为类型没有名字 // struct ??? stu3; // 编译失败类型名无匿名。特点没有struct标签如Student因此之后无法定义相同类型的其他变量。用途仅用于临时封装一组数据且确定不再需要同类型其他变量的情况例如全局配置表。4使用typedef定义别名工程中最常见typedef struct Student { int id; char name[20]; } Stu; // Stu 是 struct Student 的别名 // 下面两种写法等价 struct Student stu1; Stu stu1;类型名struct Student和Stu都合法。2枚举enum①什么是枚举写代码时我们经常会用数字代表某种状态或类别。比如int day 1; // 1代表星期一过几天再看你可能就忘了1是星期一还是星期日。要是写成int day MONDAY;是不是一眼就懂枚举就是用来把数字变成有意义名字的工具。②如何定义枚举语法很简单enum 类型名 {名字1,名字2,名字3};比如定义一个星期的枚举enum Weekday {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY};系统编译器会自动给他们分配数值从0​ 开始MONDAY 是 0TUESDAY 是 1以此类推你也可以手动指定enum Weekday {MONDAY 1,TUESDAY,WEDNESDAY};这样MONDAY是 1TUESDAY是 2WEDNESDAY是 3以此类推③声明 初始化enum Weekday today WED;声明enum Weekday today; → 告诉编译器“我有个变量叫 today它的类型是 enum Weekday”。初始化在声明的同时给它一个值 WED也就是 3。④遍历for (enum Weekday d MON; d SUN; d) { printf( %d\n, d); }在 for 里直接声明变量​enum Weekday d MON—— 这是 C99 标准的写法意思是“定义一个枚举变量 d从 MON(1) 开始”。现在很多编译器都支持很常用。为什么能d​因为枚举底层是整数d就让 d 从 1 变 2、2 变 3……直到 7。遍历的前提​这种写法只有当枚举值是连续整数时才安全。如果你的枚举是enum Test { A1, B5, C10 };那for (dA; dC; d)就会打出 1 2 3 4 5 … 10其中很多值根本没定义容易出怪事。所以你的MON~SUN连续完美适配遍历。⑤用作状态机切换// 枚举状态机 enum State { IDLE, // 默认 0 RUNNING, // 1 PAUSED, // 2 STOPPED // 3 };int main() { // 声明 初始化 enum State curr IDLE; // 状态切换 curr RUNNING; printf(\ncurr - RUNNING %d\n, curr); curr STOPPED; printf(curr - STOPPED %d\n, curr); return 0; }3共用体union①什么是共用体共用体union是 C/C 中的一种复合数据类型它允许多个不同数据类型的成员共享同一块内存空间。与结构体struct为每个成员分配独立内存不同共用体的所有成员从同一个起始地址开始存储因此在任意时刻**只能有效地存储其中一个成员的值。union Data { int i; float f; char c; };上述共用体Data包含三个成员但它们共用同一块内存。假设在 32 位系统中int占 4 字节float占 4 字节char占 1 字节那么union Data的大小通常为 4 字节取最大成员所占内存的大小并考虑内存对齐。②共用体和结构体的区别③union怎么创建union Data { int a; float b; };此时和 struct 完全一样只是定义了一种类型没有定义变量。union Data data;申请变量以后才占内存。因此union 和 struct 在定义、声明变量方面几乎没有区别。示例union REG { uint32_t value; struct { unsigned EN : 1; unsigned MODE : 2; unsigned READY : 1; } bits; };union REG { uint32_t value;意思就是我准备定义一种新的类型名字叫REG它里面第一个成员value 类型为uint32_t然后又定义了一个匿名结构体。struct { unsigned EN : 1; unsigned MODE : 2; unsigned READY : 1; }这就是一个普通结构体只不过它们都是位域。你现在不用管位域。就把它看成struct { EN MODE READY }最后} bits;是什么意思其实就是struct { ... } bits;等价于struct Temp { ... }; struct Temp bits;把定义和声明写一起了翻译成人话创建了一个成员变量名字叫 bits。所以整个 union可以先理解成定义了一个REG共用体类型。union REG { uint32_t value; struct XXX { EN MODE READY } bits; };定义类型但是没有定义变量union REG reg; 此后才会申请内存初始化reg.value 0x13; 或reg.bits.EN 1;这样同一个内存提供了两种访问方式寄存器可以选择32位全配置也可以配置其中1/2位这就是通过union 共用体实现的。④union干什么用的struct 很符合直觉一个人有姓名、年龄、身高。union 就变成了同一个抽屉你可以把它当袜子抽屉也可以当内裤抽屉但它始终只有一个抽屉。关键就是一句话union 不是存多个数据而是对同一块内存有多种解释方式。举例structstruct Data { int a; float b; };内存中sizeofData8 字节a 有自己的空间b有自己的空间互不影响。再看unionunion Data { int a; float b; };内存只有这一块a 和b 均指向此块地址sizeofData4字节所以 union任何时刻只能可靠地保存一个成员的值。意义如果只是为了存数据struct 更好。union 存在的意义不是存更多东西而是换一种角度看同一份数据。1拆字节union { uint32_t value; uint8_t byte[4]; } data;赋值data.value 0x12345678;内存就是78563412而对于byte[0]看到的是0x78 byte[1]看到的是0x56同一块内存一次按32位看一次按8位看。为什么嵌入式要这样例如 UART。收到78 56 34 12四个字节最终想得到uint32_tunion 就很好用。2寄存器union提供了一块内存两种视角位域 struct提供了把一个 32 位整数拆成有名字的 bit 段编译器帮你把改 bit0翻译成对应的位操作你不用手写value | (1 0)这种容易出错的代码。写的和读的都是同一块 4 字节内存value是整体视角bits是逐位视角谁改谁生效。那其实结构体也能实现这个功能吗3协议解析例如 CAN 收到8Byte01 02 03 04 05 06 07 08有时候你想整体发uint8_t buf[8];有时候你想 一个字节一个字节代表不同意义发送于是union { uint8_t buf[8]; struct { uint16_t rpm; uint16_t speed; uint32_t distance; } data; };解析程序那边设计两种接受方法接受全部/ 接受一个字节 实习功能4typedef① 给结构体起别名struct Point { // Point 是结构体标签tag不是类型名 int x; int y; }; struct Point p1; // 定义变量时必须带 struct p1.x 10; p1.y 20;struct Point { int x; int y; } p1; // 这里直接定义了变量 p1 /* 后续还能继续定义同类型变量 */ struct Point p2; p1.x 10; p2.x 30;struct { // 没有标签名 int x; int y; } p1; // 只有这一个变量 p1.x 10; p1.y 20; /* 错误无法再定义同类型变量因为类型没有名字 */ // struct ??? p2; // 编译失败用typedeftypedef struct { int x; int y; } Point; // Point 是结构体类型的新名字 Point p1; // 等价于 struct { ... } p1; p1.x 10; p1.y 20;② 给函数指针起名字typedef int (*Calc)(int, int); // Calc 代表“指向返回int、接收两个int的函数的指针” int add(int a, int b) { return a b; } int mul(int a, int b) { return a * b; } Calc func add; // func 指向 add int r1 func(3, 5); // r1 8 func mul; // 改指向 mul int r2 func(3, 5); // r2 15③ 给复杂类型起名字例如数组typedef int Vec4[4]; // Vec4 表示“4个int的数组” Vec4 v; // 完全等价于 int v[4]; v[0] 1; v[1] 2; v[2] 3; v[3] 4;④ 给基本类型起别名typedef unsigned int uint32; // 明确位宽语义 typedef unsigned char byte; // 表达“字节”含义 uint32 counter 1000; byte buffer[8];⑤ 给枚举起别名enum Color { RED, GREEN, BLUE }; enum Color bg GREEN; // 这里必须写 enum Colorenum Color { RED, GREEN, BLUE } bg; // 直接定义了一个变量 bg类型是 enum Color // 后续赋值 bg GREEN;enum { RED, GREEN, BLUE }; int bg GREEN; // RED/GREEN/BLUE 只是整型常量不再是“枚举类型变量”匿名枚举无法再定义同类型的枚举变量因为类型没有名字。用了typedeftypedef enum { RED, GREEN, BLUE } Color; // Color 成为枚举类型的别名 Color bg GREEN; // 不用写 enum { ... }⑥ 给 union 起别名union Num { int i; float f; }; union Num data; data.i 0x41A00000;union Num { int i; float f; } data; // 直接定义变量 data data.i 0x41A00000;union { int i; float f; } data; // 只有这一个变量之后不能再定义同类型变量 data.i 0x41A00000;用了typedeftypedef union { int i; float f; } Num; // Num 是联合体的别名 Num data; data.i 0x41A00000; // 十六进制整数 printf(%f\n, data.f); // 以浮点解析输出 20.0示例值C 语言规定结构体struct、枚举enum、共用体的标签union本身不是类型名必须配合struct/enum/union才构成完整类型名。typedef 的作用就是把这个“带关键字的完整类型”映射成一个简单的别名从而在定义变量时少写关键字。总结typedef不创建新类型它只是给已有类型取一个新名字。结构体别名、枚举别名、函数指针别名这三类就是STM32最常见、最有价值的用法。