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

目录

复合数据类型

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

  • 特点:在声明结构体末尾的}后直接跟变量列表,变量stu1stu2立即分配内存。

  • 用途:当某个类型只在局部使用,或者想强调这些变量的初始存在时使用。

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 StudentStu都合法。

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 Color
enum 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最常见、最有价值的用法。