1. 指针函数
本质:返回值是指针的函数
c
// 定义:函数名先和()结合,返回
int* int* fun(int a, int b);
字节数 :函数本身不占内存,返回的指针占 4/8 字节(32 位系统 4 字节,64 位系统 8 字节)
8位系统:51单片机,指针占2个字节
32位系统:STM32,ESP32,指针占4字节
64位系统:极少,工业高端芯片,指针占8个字节
32 位 Windows/Linux**所有指针统一占 4 字节。
64 位 Windows/Linux(现在主流)所有指针统一占 8 字节。
底层原理:返回值是一个内存地址(RAM / 寄存器地址)
真实用途
- 返回缓冲区首地址
- 返回硬件数据首地址
c
// 底层驱动:返回接收缓冲区指针
uint8_t* UART_GetRxBuffer(void) {
return &uart_rx_buf[0]; }
2. 函数指针
本质:指向函数的指针变量
c
// 定义:指针先和*结合,再指向函数
int (*pfun)(int a, int b);
字节数 :固定 4/8 字节(所有指针都一样)
底层原理
- 函数名 = 函数在 Flash 中的地址
- 函数指针 = 存这个地址的变量
真实用途(必须用)
- 中断回调函数
- 硬件驱动统一接口
- 状态机、调度器
- bootloader 跳转应用程序
真实底层代码(中断回调)
c
// 定义函数指针类型
typedef void (*CallbackFunc)(void);
// 底层驱动注册中断回调
void GPIO_RegisterIRQ(CallbackFunc cb) {
// 硬件触发时,自动调用你传入的函数
pCallback = cb;
}
// 上层应用写自己的中断函数
void my_handler(void) {
// 点灯、读按键
}
// 注册
GPIO_RegisterIRQ(my_handler);
为什么底层必须用函数指针?
- 底层驱动不知道上层要干嘛
- 只提供钩子(函数指针)
- 硬件触发 → 调用地址 → 执行用户逻辑
3. 数组指针(行指针)
本质:指向一维数组的指针
c
// 定义:指针先和*结合,再指向数组
int (*p)[5]; // 指向包含5个int的一维数组
字节数 :固定 4/8 字节
- 底层原理
int (*p)[10];指向一整行数组 ,用于二维数组(屏显存、表格、寄存器表)
- 真实用途
- LCD 显存操作
- Flash 分区表
- 多行配置参数
c
// LCD 一行 800 像素
uint16_t (*line_ptr)[800] = (uint16_t(*)[800])0xC0000000;
4. 指针数组
本质:存放指针的数组
c
// 定义:[]优先级高,先和数组结合
int *p[5]; // 数组里有5个int*类型指针
字节数 :元素个数 × 指针大小
- 例:
int *p[5]→ 5×8=40 字节(64 位)
底层原理
类型* 数组名[N]里面存 N 个地址
真实用途
- 多个设备的寄存器基地址
- 多个回调函数表
- 字符串表(日志、命令)
c
// 3个串口基地址
uint32_t* uart_base[] = {
(uint32_t*)0x40001000,
(uint32_t*)0x40002000,
(uint32_t*)0x40003000
};
5. 各数据类型字节数(64 位系统)
| 类型 | 字节数 |
|---|---|
| char | 1 |
| short | 2 |
| int / long | 4 |
| long long | 8 |
| float | 4 |
| double | 8 |
| 所有指针类型 | 8 |
32 位系统:所有指针都是 4 字节
嵌入式真实底层规则: 绝对不用 int、long,只用 stdint.h 固定大小类型!
6. 排序算法
冒泡排序(标准模板)
c
void bubbleSort(int arr[], int n) {
// 外层:控制轮数,n-1轮
for (int i = 0; i < n - 1; i++) {
// 内层:两两比较
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
-
核心:相邻比较,大的往后冒
-
时间复杂度:O(n²)
-
真实用途:
- 按键扫描排序
- ADC 采样去抖(取中间值)
- 数据滤波
嵌入式不会拿来排序大数据,只用来处理小批量硬件数据。
7. 关键字
const 用法(3 句记完)
- const int a :常量,值不能改
- *const int p :指针指向的内容不能改,指针本身能改
- *int const p :指针本身地址不能改,指向内容能改
口诀:const 后面跟谁,谁就不能改
底层原理
const变量存在 Flash- 非 const 存在 RAM
嵌入式 RAM 极小(1~20KB),能放 Flash 绝不放 RAM。
真实用法
c
// 存在Flash,不占RAM
const uint8_t log_table[] = "AT+CONNECT\r\n";
const uint32_t PIN_CONFIG = 0x00010001;
const = 放 Flash,省 RAM
static 用法(3 大核心)
- 修饰局部变量 :延长生命周期,只初始化一次
- 修饰全局变量 :只能在本文件使用,外部文件不可见
- 修饰函数 :只能在本文件调用,外部文件不可见
底层原理
- static 局部变量 :存在 RAM 全局区,不释放
- static 全局变量 / 函数 :只在本文件可见(防止命名冲突)
真实用途(驱动必写)
c
/// 只能本文件调用,防止外部乱改
static void GPIO_Config(void) {
// 操作寄存器
}
// 中断计数,不会被销毁
static uint32_t irq_count = 0;
为什么底层必须用 static?
- 嵌入式文件多
- 防止函数重名
- 保护硬件状态不被外部篡改
- 中断变量必须 static
static = 保护硬件、防止冲突、中断变量
8. 先判断再遍历 vs 先遍历再判断
1. 先判断再遍历(嵌入式首选)
c
if(条件){
循环遍历; }
优点:
- 条件不满足时,直接跳过循环,效率极高
- 代码逻辑清晰,减少无效循环
缺点:
- 必须提前知道条件,无法在循环中动态判断
底层原理
- 不满足条件不进循环
- 省电、省 CPU
- 嵌入式 CPU 资源极低,必须这样写
真实场景
c
if(UART_RX_READY()) {
// 有数据才遍历
for(int i=0; i<len; i++) {
buf[i] = UART->DR;
}
}
2. 先遍历再判断(极少用)
c
循环遍历{
if(条件){
执行操作;
}
}
优点:
- 可以遍历中动态判断,灵活处理每个元素
- 适合不知道满足条件的位置的场景
缺点:
- 每次都要进入循环,无效循环多,效率低
- CPU 一直跑
- 费电、发热、占用资源
- 只有轮询按键、屏幕刷新才偶尔用
C语言 位操作 清除第三位