目录
复合数据类型
1:结构体
①:什么是结构体
②:定义结构体变量的两种方式
③:访问结构体成员
④:结构体初始化
⑤:结构体数组
⑥:结构体指针传参
⑦:用 typedef 简化结构体类型名Struct
⑧:struct的四种写法
1:
2:
3:
4:
2:枚举enum
①:什么是枚举
②:如何定义枚举
③:声明 + 初始化
④:遍历
⑤:用作状态机切换
3:共用体union
①:什么是共用体
②:共用体和结构体的区别
③:union怎么创建
④:union干什么用的
1:拆字节
2:寄存器
3:协议解析
4:typedef
① 给结构体起别名
② 给函数指针起名字
③ 给复杂类型起名字(例如数组)
④ 给基本类型起别名
⑤ 给枚举起别名
⑥ 给 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叫做结构体标签,相当于这个结构体的类型,
比如Dog,Cat,Book,有点像python中的类
3:花括号里面是什么?
{ int id; char name[20]; float score; }这里叫:成员/成员变量,它们就是 Student 里面的数据。
4:第四部分
struct Student stu1, stu2;这一句才是真正创建变量。
相当于创建两个 Student 类型变量名字分别叫:stu1,stu2
就像int a,b ;这里只不过是创建两个 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 简化结构体类型名Struct
typedef在 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 是 0,TUESDAY 是 1,以此类推
你也可以手动指定:
enum Weekday {
MONDAY = 1,
TUESDAY,
WEDNESDAY
};
这样MONDAY是 1,TUESDAY是 2,WEDNESDAY是 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 { A=1, B=5, C=10 };
那
for (d=A; d<=C; 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 不是"存多个数据",而是"对同一块内存有多种解释方式"。
举例struct:
struct Data { int a; float b; };内存中:
sizeof(Data)=8 字节,a 有自己的空间,b有自己的空间,互不影响。
再看union:
union Data { int a; float b; };内存:
只有这一块,a 和b 均指向此块地址,sizeof(Data)=4字节
所以 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_t,union 就很好用。
2:寄存器
union提供了"一块内存,两种视角"
位域 struct提供了"把一个 32 位整数拆成有名字的 bit 段"
编译器帮你把"改 bit0"翻译成对应的位操作,你不用手写value |= (1 << 0)这种容易出错的代码。写的和读的都是同一块 4 字节内存,value是整体视角,bits是逐位视角,谁改谁生效。
那其实结构体也能实现这个功能吗?
3:协议解析
例如 CAN 收到:8Byte:01 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; };解析程序那边:设计两种接受方法,接受全部/ 接受一个字节 ,实习功能
4:typedef
① 给结构体起别名
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; // 编译失败用typedef:
typedef 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 只是整型常量,不再是“枚举类型变量”匿名枚举无法再定义同类型的枚举变量,因为类型没有名字。
用了typedef:
typedef 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;用了typedef:
typedef 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最常见、最有价值的用法。