48、回调函数:如何用函数指针实现回调?
回调函数,这名字听着挺唬人。说白了就是:你写一个函数,但你不直接调用它,而是把它的地址传给另一个函数,让那个函数在合适的时机去调用它。
我刚开始学C语言那会儿,看到“回调”两个字就头大。心想:我直接调用不就行了?干嘛要绕一圈?后来做嵌入式项目,写按键驱动、定时器处理、通信协议栈……才发现,没有回调函数,很多代码根本没法写。
嗯,今天咱们就把这个“绕一圈”的逻辑彻底讲清楚。
48.1 函数指针:回调的基石
要实现回调,首先得搞懂函数指针。函数指针,就是指向函数的指针变量。它跟普通指针不一样——普通指针存的是数据的地址,函数指针存的是函数的入口地址。
声明一个函数指针的语法,说实话有点反人类:
// 声明一个函数指针,指向返回值为int、参数为两个int的函数
int (*pFunc)(int, int);
注意那个括号!(*pFunc) 必须括起来。如果不括,写成 int *pFunc(int, int),那就变成了“声明一个返回int指针的函数”——完全是两码事。这个坑我踩过,调试了半天才发现是括号的问题。
函数指针的赋值和调用,看这个例子:
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
// 定义一个函数指针,指向add函数
int (*calc)(int, int) = add;
// 通过函数指针调用函数
int result = calc(3, 5);
printf("结果: %d\n", result); // 输出 8
return 0;
}
这里 calc 就是函数指针,它指向 add 函数。调用 calc(3, 5) 等价于调用 add(3, 5)。
typedef 给函数指针起个别名,代码会清爽很多。比如 typedef int (*CalcFunc)(int, int);,之后直接用 CalcFunc calc = add; 就行了。
48.2 回调函数的典型模式
回调函数的核心模式其实就三步:
- 定义回调函数的类型(用函数指针类型)
- 写一个“注册”函数,接收函数指针作为参数
- 在合适的时机调用回调
我画了一张图,帮你理解这个流程:
看明白了吗?主调函数不关心回调函数里具体做了什么,它只负责在事件发生时“通知”你。这种解耦的设计,在嵌入式系统里太常见了。
48.3 实战:用回调实现一个定时器框架
我在做物联网网关项目时,需要管理多个定时任务。每个任务到期后要执行不同的操作——有的要上报数据,有的要重启设备,有的要切换工作模式。如果写一堆 if-else,代码会变成一坨屎山。
用回调函数,代码就优雅多了:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // sleep函数
// 定义回调函数类型
typedef void (*TimerCallback)(void* context);
// 定时器结构体
typedef struct {
int id;
int interval; // 定时时间(秒)
int remaining; // 剩余时间
TimerCallback callback; // 回调函数指针
void* context; // 回调参数
int active; // 是否激活
} Timer;
// 定时器管理器
typedef struct {
Timer timers[10];
int count;
} TimerManager;
// 初始化定时器管理器
void timer_init(TimerManager* mgr) {
mgr->count = 0;
}
// 注册一个定时器
int timer_register(TimerManager* mgr, int id, int interval,
TimerCallback cb, void* ctx) {
if (mgr->count >= 10) {
return -1; // 满了
}
Timer* t = &mgr->timers[mgr->count++];
t->id = id;
t->interval = interval;
t->remaining = interval;
t->callback = cb;
t->context = ctx;
t->active = 1;
return 0;
}
// 定时器滴答(每秒调用一次)
void timer_tick(TimerManager* mgr) {
for (int i = 0; i < mgr->count; i++) {
if (!mgr->timers[i].active) continue;
mgr->timers[i].remaining--;
if (mgr->timers[i].remaining <= 0) {
// 时间到!调用回调函数
if (mgr->timers[i].callback) {
mgr->timers[i].callback(mgr->timers[i].context);
}
// 重置定时器(循环定时)
mgr->timers[i].remaining = mgr->timers[i].interval;
}
}
}
// ========== 用户自定义的回调函数 ==========
void report_data(void* ctx) {
int sensor_id = *(int*)ctx;
printf("[回调] 传感器 %d 上报数据: 温度 25.3°C\n", sensor_id);
}
void reboot_device(void* ctx) {
printf("[回调] 设备重启中...\n");
}
void switch_mode(void* ctx) {
char* mode = (char*)ctx;
printf("[回调] 切换到工作模式: %s\n", mode);
}
int main() {
TimerManager mgr;
timer_init(&mgr);
int sensor_id = 1;
char mode[] = "节能模式";
// 注册三个定时任务
timer_register(&mgr, 1, 3, report_data, &sensor_id); // 每3秒上报
timer_register(&mgr, 2, 10, reboot_device, NULL); // 每10秒重启
timer_register(&mgr, 3, 7, switch_mode, mode); // 每7秒切换模式
// 模拟运行20秒
for (int i = 0; i < 20; i++) {
printf("第 %d 秒: ", i + 1);
timer_tick(&mgr);
sleep(1);
}
return 0;
}
void* context 这个参数。回调函数需要知道“谁触发了它”,但又不能提前知道具体是什么类型。用 void* 传递上下文,在回调函数里再转换成实际类型——这是C语言里非常经典的做法。
48.4 回调函数的常见应用场景
回调函数在嵌入式系统里无处不在。我列几个最常见的:
| 场景 | 说明 | 典型例子 |
|---|---|---|
| 中断处理 | 硬件中断发生时,调用注册的回调 | GPIO中断、定时器中断 |
| 定时器任务 | 定时时间到,执行回调 | 心跳检测、超时重传 |
| 通信协议 | 收到数据包后,调用解析回调 | UART接收、TCP数据到达 |
| 状态机 | 状态切换时,调用进入/退出回调 | 按键状态机、协议状态机 |
| 排序/搜索 | 标准库的 qsort 需要比较回调 |
qsort(arr, n, size, cmp_func) |
你看 qsort 这个函数,它就是一个典型的回调应用。你只需要提供一个比较函数,qsort 就帮你把数组排好序。至于它内部是用快排还是归并排序,你根本不用关心。
48.5 避坑指南
- 回调函数里不要做阻塞操作——比如在中断回调里调用
printf或malloc。中断上下文里做这些事,轻则死机,重则烧板子。我有个同事就因为这个,把一块开发板的串口驱动搞崩了三次。 - 检查回调指针是否为NULL——调用回调之前,一定要判断
if (callback != NULL)。否则注册时忘了传回调,程序直接跑飞。 - 注意线程安全——如果回调在多线程环境下被调用,回调函数里访问的全局变量要做好同步保护。用互斥锁或者关中断,看具体场景。
- 回调函数的生命周期——确保回调函数在调用时仍然有效。如果回调函数是某个对象的成员函数,对象已经被销毁了还去调用,那就是典型的“悬空指针”问题。
48.6 回调 vs 直接调用:什么时候用?
你可能会问:什么时候该用回调,什么时候直接调用就行?
我的经验是:如果你不确定“什么时候”要执行某个操作,就用回调。比如:
- 你写了一个按键驱动,但不知道用户按下键后想干什么——用回调,让用户自己注册处理函数。
- 你写了一个网络库,但不知道收到数据后怎么处理——用回调,让上层应用决定。
- 你写了一个排序函数,但不知道比较规则——用回调,让调用者提供比较函数。
反过来,如果调用关系是固定的、确定的,直接调用更简单直接。别为了用回调而用回调,那叫过度设计。
回调函数,说白了就是“把函数当作参数传递”。它让代码变得灵活、可扩展、低耦合。在嵌入式系统里,从按键处理到协议栈,从定时器到任务调度,回调无处不在。掌握了它,你的C语言水平会上一个台阶。
公众号:蓝海资料掘金营,微信deep3321