C语言基础
🎯 主要学习:数据类型、宏定义、typedef、结构体、枚举、循环、数组、指针
前五项是 C 语言的"语法地基"(怎么描述数据、怎么组织代码),后三项是嵌入式开发的核心三件套:循环负责重复任务(如轮询传感器),数组负责批量数据(如采样缓冲),指针负责直接操作硬件地址(寄存器、DMA、外设)。
🔢 一、数据类型(Data Types)
1. 基本数据类型
| 类型 | 关键字 | 32位平台大小 | 取值范围 | 嵌入式用途 |
|---|---|---|---|---|
| 字符型 | char | 1字节 | -128 | 存 ASCII、小整数 |
| 短整型 | short | 2字节 | -32768~32767 | 省内存的小整数 |
| 整型 | int | 4字节 | -2^31~2^31-1 | 通用整数 |
| 长整型 | long | 4字节 | 同 int | 兼容历史代码 |
| 单精度浮点 | float | 4字节 | 小数计算(STM32 无FPU时很慢) | |
| 双精度浮点 | double | 8字节 | 精度更高 | 一般不用,占内存 |
⚠️ 嵌入式里尽量用固定宽度类型(见下),不要依赖
int的大小,因为不同平台位数不同。
2. 嵌入式推荐:固定宽度整数类型(stdint.h)
STM32 标准库和 HAL 库都用这套,大小明确、跨平台一致:
| 类型 | 大小 | 范围 | 常用场景 |
|---|---|---|---|
uint8_t | 1字节无符号 | 0~255 | 寄存器、GPIO 引脚号、缓冲区索引 |
int8_t | 1字节有符号 | -128~127 | 小范围有符号数 |
uint16_t | 2字节无符号 | 0~65535 | ADC 值(0~4095)、PWM 比较值 |
int16_t | 2字节有符号 | -32768~32767 | 温度、加速度等有符号传感器 |
uint32_t | 4字节无符号 | 0~2^32-1 | 定时器计数、地址、时间戳 |
int32_t | 4字节有符号 | -2^31~2^31-1 | 通用计算 |
#include <stdint.h>
uint8_t led_state = 0; // LED 状态,非0即1
uint16_t adc_value = 0; // ADC 采样值
uint32_t tick = 0; // 系统滴答计数
3. 数据类型完整对照表(含 ST 库别名)

🔑 这张表把 C 原生关键字 / 位数 / 取值范围 / stdint 类型 / ST 库类型 五列对齐,是嵌入式开发最常用的速查表。
ST 库别名(标准外设库 SPL 里大量使用):
u8/s8=uint8_t/int8_t(无符号/有符号 8 位)u16/s16=uint16_t/int16_tu32/s32=uint32_t/int32_t读 STM32 标准库源码时经常看到
u32、u16这类写法,本质就是 typedef 出来的固定宽度类型,和uint32_t完全等价。新项目推荐用stdint.h的标准写法(可移植性更好),但要能看懂 ST 库的别名。
4. 修饰符
| 关键字 | 作用 | 嵌入式场景 |
|---|---|---|
const | 定义常量,不可修改 | 查表数据、字模(省 RAM,放 Flash) |
static | 函数内:保留值不销毁;文件内:限制作用域 | 状态变量、私有函数 |
volatile | 禁止编译器优化,每次都从内存读 | 寄存器、中断变量、多线程共享(必加!) |
unsigned | 无符号 | 计数、地址、位运算 |
const uint8_t font[][16] = {...}; // 字模放 Flash,不占 RAM
static uint8_t state = 0; // 函数调用间保留状态
volatile uint8_t uart_rx_flag = 0; // 中断里会改,必须 volatile
🏷️ 二、宏定义(#define)
1. 基本用法
#define PI 3.14159
#define BUF_SIZE 64
#define LED_PIN GPIO_Pin_13
🔑 宏定义是纯文本替换,在预处理阶段完成,不占内存、没有类型检查。末尾不要加分号(加了分号会把分号也替换进去)。
2. 带参数的宏(类似函数,但更快)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) > 0 ? (x) : -(x))
uint16_t m = MAX(adc1, adc2); // 取较大值
⚠️ 每个参数都要加括号,防止运算符优先级出问题。宏没有类型检查,复杂逻辑优先用 inline 函数。
3. 嵌入式常用宏
// 位操作宏(操作寄存器最常用)
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
#define CLR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
#define TGL_BIT(reg, bit) ((reg) ^= (1 << (bit)))
#define GET_BIT(reg, bit) (((reg) >> (bit)) & 1)
// 条件编译(调试开关、多平台适配)
#define DEBUG 1
#if DEBUG
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#define LOG(fmt, ...)
#endif
4. 头文件保护(防止重复包含)
#ifndef __LED_H
#define __LED_H
// 头文件内容...
#endif
每个 .h 文件都要有这个结构,否则重复包含会报重定义错误。
🔤 三、typedef(类型别名)
1. 基本用法
typedef unsigned char uint8; // 给 unsigned char 起个短名
typedef int (*FuncPtr)(int); // 给函数指针类型起别名
🔑
typedef是给已有的类型起别名,不是定义新类型。和#define不同,typedef 有类型检查,由编译器处理。
2. 嵌入式中最常见:给结构体起别名
typedef struct
{
uint16_t x;
uint16_t y;
} Point_t; // 以后直接用 Point_t 定义变量,不用写 struct
Point_t p1 = {10, 20};
STM32 库里到处都是这种写法:
GPIO_InitTypeDef、TIM_TimeBaseInitTypeDef都是 typedef 出来的结构体类型。
3. typedef vs #define
| 对比 | typedef | #define |
|---|---|---|
| 处理时机 | 编译器 | 预处理器(文本替换) |
| 类型检查 | ✅ 有 | ❌ 无 |
| 指针类型 | typedef char *pchar; 正确 | #define pchar char * 会出问题 |
📦 四、结构体(struct)
1. 定义与使用
// 定义结构体类型
struct Sensor
{
uint16_t temp; // 温度
uint16_t humi; // 湿度
uint32_t timestamp;
};
struct Sensor s1; // 定义变量(要写 struct)
s1.temp = 250; // 用 . 访问成员
s1.humi = 600;
2. 配合 typedef(嵌入式标准写法)
typedef struct
{
uint16_t temp;
uint16_t humi;
uint32_t timestamp;
} Sensor_t;
Sensor_t s2 = {250, 600, 1000}; // 初始化按顺序
Sensor_t s3 = {.temp = 250, .humi = 600}; // 指定成员初始化(C99,推荐)
3. 结构体指针与 -> 运算符
Sensor_t *p = &s2;
p->temp = 300; // 指针用 -> 访问成员,等价于 (*p).temp
p->timestamp = 2000;
🔑 结构体作函数参数时,传指针比传值好——传值会拷贝整个结构体(占栈、费时间),传指针只传 4 字节地址。STM32 库的
GPIO_Init(GPIOx, &gpio)就是传指针。
4. 结构体嵌套(描述复杂对象)
typedef struct
{
uint16_t x;
uint16_t y;
} Point_t;
typedef struct
{
Point_t pos; // 嵌套另一个结构体
uint8_t id;
uint8_t type;
} Device_t;
Device_t dev;
dev.pos.x = 100; // 逐层访问
5. 结构体在嵌入式中的用途
| 用途 | 示例 |
|---|---|
| 封装外设配置 | STM32 的 GPIO_InitTypeDef |
| 描述一个设备 | 传感器数据 + 状态 + 配置 |
| 链表节点 | 数据队列、任务管理 |
| 寄存器映射 | 用结构体描述一组寄存器地址 |
🔢 五、枚举(enum)
1. 定义与使用
// 定义枚举类型
typedef enum
{
LED_OFF = 0,
LED_ON = 1,
LED_BLINK = 2
} LED_State_t;
LED_State_t state = LED_ON;
🔑 枚举本质是整数,第一个成员默认从 0 开始,依次 +1。显式赋值后,后面的成员在前一个基础上 +1。
2. 默认值规则
typedef enum
{
RED, // = 0
GREEN, // = 1
BLUE = 5, // = 5
YELLOW // = 6(自动+1)
} Color_t;
3. 嵌入式典型用途:状态机
typedef enum
{
STATE_IDLE, // 空闲
STATE_READING, // 读取中
STATE_PROCESS, // 处理中
STATE_ERROR // 错误
} SystemState_t;
SystemState_t g_state = STATE_IDLE;
void State_Machine(void)
{
switch(g_state)
{
case STATE_IDLE: /* ... */ break;
case STATE_READING: /* ... */ break;
case STATE_PROCESS: /* ... */ break;
case STATE_ERROR: /* ... */ break;
}
}
💡 枚举比
#define一堆常量更清晰,还能把相关的状态归为一组,编译器有时还能帮你检查 switch 是否漏了分支。
4. 枚举 vs 宏定义
| 对比 | enum | #define |
|---|---|---|
| 类型 | 有类型(整数) | 无类型(文本替换) |
| 作用域 | 有作用域 | 全局(直到 #undef) |
| 自动编号 | ✅ 支持 | ❌ 需手动 |
| 调试可见 | ✅ 调试器能显示枚举名 | ❌ 只显示数字 |
🔁 六、循环(Loop)
1. 三种循环对比
| 循环 | 语法 | 适用场景 |
|---|---|---|
| for | for(初始;条件;更新){} | 已知次数(遍历数组、固定延时) |
| while | while(条件){} | 次数不定(等待某标志位) |
| do-while | do{}while(条件); | 至少执行一次(菜单、初始化重试) |
2. for 循环(已知次数)
结构:for(初始化表达式; 循环条件; 更新表达式) { 循环体 }
执行顺序:①初始化(只跑一次)→ ②判断条件 → ③执行循环体 → ④更新表达式 → 回到②
// 遍历数组求和(最常用)
uint16_t sum = 0;
for(uint8_t i = 0; i < 10; i++) // i 从 0 到 9
{
sum += adc_buf[i];
}
// 固定次数的软件延时
for(uint32_t i = 0; i < 1000000; i++)
{
__NOP(); // 空操作,占一个时钟周期
}
// 倒序遍历(如数据搬移)
for(uint8_t i = 9; i > 0; i--)
{
buf[i] = buf[i - 1];
}
💡
for的三个表达式都可省略,但分号不能省:for(;;){}等价于while(1)死循环。
3. while 循环(次数不定 / 先判断后执行)
结构:while(循环条件) { 循环体 }
特点:先判断条件,为真才执行;条件一开始就为假则一次都不跑。
// 等待某个标志位(如等待转换完成)
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET)
{
// 什么都不做,原地等
}
// 统计数据直到遇到结束符 0xFF
uint8_t i = 0;
while(rx_buf[i] != 0xFF)
{
data[i] = rx_buf[i];
i++;
}
// 主循环(嵌入式最常见)
while(1) // 条件恒真,永不退出
{
// 任务...
}
4. do-while 循环(至少执行一次 / 先执行后判断)
结构:
do
{
循环体
} while(循环条件); // ⚠️ 末尾必须有分号!
特点:先执行一次循环体,再判断条件;无论条件真假,至少跑一次。
// 菜单程序:先显示一次菜单,再根据输入决定是否继续
uint8_t key;
do
{
Show_Menu(); // 显示菜单(至少显示一次)
key = Key_Scan(); // 扫描按键
Handle_Key(key); // 处理按键
} while(key != KEY_EXIT); // 按退出键才离开
// 初始化重试:先尝试一次,失败再重试
uint8_t retry = 0;
do
{
status = Sensor_Init(); // 先初始化一次
retry++;
} while(status != 0 && retry < 3); // 失败且未超3次则重试
// 字符串长度计算(至少处理一个字符)
uint8_t len = 0;
do
{
len++;
} while(str[len] != '\0');
5. 三种循环怎么选
| 场景 | 推荐 | 原因 |
|---|---|---|
| 已知循环次数(遍历数组、固定延时) | for | 初始化/条件/更新集中在一起,清晰 |
| 次数不定、可能一次都不执行(等标志位) | while | 先判断,条件不满足直接跳过 |
| 必须至少执行一次(菜单、重试、协议交互) | do-while | 先执行后判断,保证跑一次 |
🔑 三者可以互相改写,但选对了代码更易读。嵌入式里
while(1)主循环、while(标志)等待、do-while菜单/重试 是最典型的三种用法。
6. 嵌入式中最典型的循环 —— 主循环
int main(void)
{
System_Init(); // 初始化(只跑一次)
while(1) // 主循环:程序永不退出
{
Read_Sensor(); // 读取传感器
Process_Data(); // 处理数据
Update_Display(); // 刷新显示
}
}
💡 单片机没有操作系统时,
while(1)就是程序的"心脏",所有任务在里面轮流执行(轮询式架构)。
7. break / continue
| 关键字 | 作用 |
|---|---|
| break | 立即跳出整个循环 |
| continue | 跳过本次剩余语句,进入下一次循环 |
for(int i = 0; i < 10; i++)
{
if(sensor[i] == 0) continue; // 该点无效,跳过
if(sensor[i] > 100) break; // 超阈值,停止扫描
sum += sensor[i];
}
8. ⚠️ 循环延时的坑
// 阻塞式延时:简单但会卡死主循环,实际项目慎用
void Delay(uint32_t n) { while(n--); }
阻塞延时期间 CPU 什么都干不了。工程中优先用定时器 + 标志位做非阻塞延时(见 STM32F103C8T6 定时器部分)。
📦 七、数组(Array)
1. 定义与访问
uint8_t buf[10]; // 声明 10 个元素,下标 0~9
uint8_t rx[5] = {0}; // 全部初始化为 0
uint8_t cmd[] = {0xAA, 0x55}; // 自动推断长度为 2
buf[0] = 25; // 下标从 0 开始!
uint8_t last = buf[9]; // 最后一个是 [9] 不是 [10]
⚠️ 最常见 bug:越界访问。
buf[10]已经越界,会覆盖相邻内存,在单片机上可能直接跑飞。
2. 数组在嵌入式中的核心用途
| 用途 | 示例 |
|---|---|
| 接收缓冲区 | uint8_t uart_rx[64]; 存串口数据 |
| 采样数据 | uint16_t adc_val[100]; 存ADC采样 |
| 滑动滤波 | 存最近 N 次采样求平均,去毛刺 |
| 查表法 | 用数组存正弦表/字模,省计算 |
3. 数组名的本质
uint8_t buf[10];
// buf 本身就是"首元素地址",即 &buf[0]
// 所以 buf[i] 等价于 *(buf + i)
🔑 这一点是数组与指针的桥梁,也是理解下一节的关键。
4. 二维数组(如OLED字模、矩阵键盘)
uint8_t keymap[4][4] = { // 4x4 矩阵键盘
{1, 2, 3, 10},
{4, 5, 6, 11},
{7, 8, 9, 12},
{15, 0, 14, 13}
};
uint8_t key = keymap[row][col];
🎯 八、指针(Pointer)—— 嵌入式的灵魂
1. 基本概念
int a = 10;
int *p = &a; // p 存放 a 的地址(& 取地址)
*p = 20; // *p 访问 p 指向的值(* 解引用)→ a 变成 20
| 符号 | 含义 |
|---|---|
&a | 取变量 a 的地址 |
*p | 取指针 p 指向的值 |
int *p | 声明一个指向 int 的指针 |
2. 指针与数组
uint8_t buf[5] = {10, 20, 30, 40, 50};
uint8_t *p = buf; // 数组名即首地址,无需 &
*p // = buf[0] = 10
*(p+1) // = buf[1] = 20
p[2] // = buf[2] = 30 (指针也能用下标!)
3. 指针作为函数参数(传址 vs 传值)
// ❌ 传值:函数内改不了外部变量
void add(int x) { x += 1; }
// ✅ 传址:通过指针修改外部变量
void add(int *x) { (*x) += 1; }
int n = 5;
add(&n); // n 变成 6
💡 数组作参数时天然"传址"(传的是首地址),所以函数里能修改整个数组。这也是
Read_Buffer(uint8_t *buf, uint16_t len)这类函数的原理。
4. 🔥 指针操作硬件寄存器(嵌入式核心)
STM32 的每个外设寄存器本质是一个固定的内存地址,用指针就能直接读写:
// GPIOC 端口输出寄存器地址(示意)
#define GPIOC_ODR (*(volatile uint32_t *)0x4001100C)
GPIOC_ODR |= (1 << 13); // PC13 输出高
GPIOC_ODR &= ~(1 << 13); // PC13 输出低
| 关键字 | 为什么需要 |
|---|---|
| volatile | 告诉编译器"这个值可能被硬件改变",禁止优化,寄存器/中断变量必须加 |
(uint32_t *) | 把地址数字强转成指针类型 |
*(...) | 解引用,直接读写该地址 |
🔑 库函数(如
GPIO_SetBits())底层就是这样操作寄存器的,只是封装得更安全易读。
5. ⚠️ 指针三大坑
| 坑 | 说明 | 对策 |
|---|---|---|
| 野指针 | 未初始化就使用 | 声明即赋 NULL 或有效地址 |
| 空指针解引用 | *p 时 p 是 NULL | 用前判断 if(p != NULL) |
| 越界/悬空 | 指向已释放或数组外 | 严格控制范围 |
📝 九、综合示例:串口接收 + 滑动滤波(三者结合)
#define BUF_SIZE 8
uint16_t adc_buf[BUF_SIZE] = {0}; // 数组:采样缓冲
uint8_t idx = 0;
// 传入指针,返回滤波后平均值
uint16_t Filter_Add(uint16_t *buf, uint16_t new_val)
{
buf[idx] = new_val; // 存入
idx = (idx + 1) % BUF_SIZE; // 循环下标(环形缓冲)
uint32_t sum = 0;
for(uint8_t i = 0; i < BUF_SIZE; i++) // 循环求和
sum += buf[i];
return sum / BUF_SIZE; // 平均值
}
一个函数里同时用到了 循环(求和)、数组(缓冲)、指针(传参),是嵌入式最常见的组合。
✅ 十、学习检查清单
- 会用
uint8_t/uint16_t/uint32_t等固定宽度类型 - 理解
const/static/volatile三个修饰符的作用 - 会写
#define常量、带参宏、位操作宏、头文件保护 - 会用
typedef给结构体起别名(XXX_t命名) - 会定义和使用结构体,理解
.和->的区别 - 会用枚举写状态机
- 能手写
while(1)主循环结构 - 掌握 for / while / do-while 三种循环的完整结构与区别
- 知道 do-while 末尾必须加分号、至少执行一次
- 会根据场景选对循环(已知次数用 for、等标志用 while、菜单/重试用 do-while)
- 理解数组下标从 0 开始、避免越界
- 分清
&(取址)和*(解引用) - 理解数组名 == 首地址
- 会用指针作函数参数修改外部变量
- 理解
volatile在寄存器/中断中的作用 - 能看懂库函数底层的指针操作
📚 十一、主要学习资料
主要跟浙江大学翁恺老师的课,科班级别的系统讲解,适合打牢基础。
翁恺 —— 浙江大学
- 课程:翁恺教你C语言程序设计!C语言基础入门!
- 特点:浙大计算机学院老师,讲解清晰、循序渐进,从变量到指针层层递进,概念讲得透,不只是教语法
- 对应重点:循环、数组、指针(本笔记三大核心)都有系统章节
- 学习建议:配合本笔记的嵌入式场景示例一起看——翁恺讲清"语言是什么",本笔记补充"在单片机里怎么用"
🔗 相关链接
- STM32F103C8T6 - 应用这些基础到实际开发板
- 自我介绍 - C语言是我的学习重点(作为嵌入式核心语言)
最后更新:2026-09-05



