C 回调函数(函数指针)完整教程
目录
1. 函数指针基础
函数名就是地址
#include <stdio.h>
void hello(void)
{
printf("Hello\n");
}
int main(void)
{
printf("hello = %p\n", hello);
printf("&hello = %p\n", &hello);
/* 输出一样,函数名就是地址 */
return 0;
}
函数名和数组名一样,在表达式中隐式 decay 成地址。hello 和 &hello 值相同,类型不同。
声明一个函数指针
/* 声明一个指向 void(void) 的函数指针 */
void (*p)(void);
p = hello; /* OK:hello decay 成地址 */
p = &hello; /* OK:等价 */
(*p)(); /* 调用:显式解引用 */
p(); /* 调用:语法糖,等价 */
口诀:"变量名先跟 * 再跟 ()"
void (*p)(void);
^ ^
| └── 圆括号表示"这是个函数指针"
└──── 星号表示"这是个指针"
/* 如果不加括号:*/
void *p(void); /* 这是个返回 void* 的函数,不是函数指针!*/
带参数的函数指针
void add(int a, int b) {
printf("%d\n", a + b);
}
void (*p)(int, int) = add;
p(3, 4); /* 输出 7 */
用 typedef 简化
/* 不用 typedef:每次写完整签名 */
void (*p1)(int, int) = add;
void (*p2)(int, int);
p2 = sub;
/* 用 typedef:变成类型名 */
typedef void (*op_func_t)(int, int);
op_func_t p1 = add;
op_func_t p2 = sub;
typedef 的读法:去掉 typedef 和末尾的名字,剩下的就是类型名
typedef void (*op_func_t)(int, int);
// 去掉 typedef 去掉名字
// 剩下:void (*)(int, int) ← 这就是 op_func_t 代表的类型
2. 回调模式
回调是函数指针最常见的应用。分三步:
1. 定义一个函数指针类型(回调的"签名")
2. 接收方:存地址,在合适的时机调用
3. 调用方:传一个符合签名的函数进去
完整例子
#include <stdio.h>
/* 1. 定义回调类型 */
typedef void (*notify_t)(int code);
/* 2. 接收方:存回调 + 触发 */
static notify_t g_cb = NULL;
void register_cb(notify_t cb)
{
g_cb = cb; /* 存函数地址 */
}
static void do_something(void)
{
/* 一些操作... */
if (g_cb) {
g_cb(42); /* 回调通知 */
}
}
/* 3. 调用方:写一个匹配的函数 */
static void my_handler(int code)
{
printf("code = %d\n", code);
}
int main(void)
{
register_cb(my_handler); /* 注册 */
do_something(); /* 触发 */
return 0;
}
内存分布
BSS/数据段 .text 段
──────────── ──────────────────────
g_cb = 0x401234 ──────→ my_handler:
push rbp
mov esi, edi
call printf
pop rbp
ret
执行 g_cb(42):
1. 从 g_cb 读取 0x401234
2. 将 42 存入 rdi(参数寄存器)
3. call 0x401234
4. 执行 my_handler 的机器码
5. ret 回到调用点
3. 相关类型
typedef 的几种常用形式:
/* 最简单的:无参数无返回值 */
typedef void (*cb_t)(void);
/* 带参数 */
typedef void (*cb_t)(int code);
typedef void (*cb_t)(int code, void *user_data);
/* 带返回值 */
typedef int (*cmp_t)(const void *a, const void *b);
/* 数组指针(用于回调表) */
typedef void (*btn_handler_t[])(void);
读法一致:去掉 typedef 和末尾的名字就是类型。
4. 回调变种
4.1 带上下文指针
给回调传一个 void *,让它能访问外部数据:
typedef void (*event_cb_t)(int code, void *ctx);
static event_cb_t g_cb;
static void *g_ctx;
void register_cb(event_cb_t cb, void *ctx)
{
g_cb = cb;
g_ctx = ctx; /* 上下文透传给回调 */
}
// 触发
if (g_cb) g_cb(42, g_ctx);
// 调用方
static void my_handler(int code, void *ctx)
{
struct my_data *d = ctx; /* 还原指针 */
printf("code=%d, name=%s\n", code, d->name);
}
struct my_data data = { .name = "sensor1" };
register_cb(my_handler, &data);
这是最常用的模式——pthread_create、qsort、LVGL 事件回调都用这种形式。
4.2 多回调(订阅模式)
#define MAX_CB 4
typedef void (*cb_t)(int);
static cb_t cbs[MAX_CB];
static int cb_cnt;
void add_cb(cb_t cb)
{
if (cb_cnt < MAX_CB)
cbs[cb_cnt++] = cb;
}
void fire(int code)
{
for (int i = 0; i < cb_cnt; i++)
cbs[i](code); /* 通知所有订阅者 */
}
4.3 回调表(多路分发)
typedef void (*btn_handler_t)(void);
void on_up(void) { printf("UP\n"); }
void on_down(void) { printf("DOWN\n"); }
void on_left(void) { printf("LEFT\n"); }
void on_enter(void) { printf("ENTER\n"); }
/* 用表格代替 switch-case */
const btn_handler_t btn_table[] = {
[KEY_UP] = on_up,
[KEY_DOWN] = on_down,
[KEY_LEFT] = on_left,
[KEY_ENTER] = on_enter,
};
/* 调用 */
void dispatch(int key)
{
if (btn_table[key])
btn_table[key](); /* 直接跳转 */
}
4.4 链式回调
typedef void (*cb_t)(int);
static cb_t g_prev = NULL;
void wrap(cb_t new_cb)
{
g_prev = new_cb; /* 存旧的 */
register_cb(my_wrapper); /* 注册新的 */
}
static void my_wrapper(int code)
{
printf("before\n");
if (g_prev) g_prev(code); /* 调到旧的 */
printf("after\n");
}
5. 常用场景
5.1 qsort —— 标准库回调
int cmp_int(const void *a, const void *b)
{
int ia = *(int*)a, ib = *(int*)b;
return (ia > ib) - (ia < ib);
}
int arr[] = {3, 1, 4, 1, 5};
qsort(arr, 5, sizeof(int), cmp_int);
// ^ 函数指针
qsort 不知道你的数组元素怎么比大小——你用回调告诉它。
5.2 pthread_create —— 线程入口
void *worker(void *arg)
{
printf("thread arg=%s\n", (char*)arg);
return NULL;
}
pthread_t tid;
pthread_create(&tid, NULL, worker, "hello");
// ^ 回调函数
// ^ 上下文指针
5.3 signal —— 系统信号
void on_sigint(int sig)
{
printf("Ctrl+C pressed\n");
_exit(0);
}
signal(SIGINT, on_sigint);
// ^ 注册回调
// 内核收到 Ctrl+C 时调它
5.4 LVGL 事件
这就是你项目里的用法:
lv_obj_add_event_cb(btn, my_handler, LV_EVENT_CLICKED, NULL);
// ^ 回调 ^ 事件类型 ^ 上下文
// 你的回调函数:
static void my_handler(lv_event_t *e)
{
/* lv_event_get_user_data(e) 可以拿上下文 */
}
5.5 长按回调
/* hal/btn-input.h 声明类型 */
typedef void (*btn_longpress_handler_t)(uint16_t code);
/* 注册:main.c */
btn_input_set_longpress(3000, on_power_longpress);
/* 触发:indev.c process_events() */
if (tick_ms() - press_tick >= longpress_ms) {
longpress_handler(held_code);
}
6. 底层原理
x86_64 汇编视角
void hello(int n) {
printf("%d\n", n);
}
int main(void) {
void (*p)(int) = hello;
p(42);
return 0;
}
编译后(AT&T 语法,简化):
# main:
leaq hello(%rip), %rax # rax = hello 的地址 (.text 段)
movq %rax, -8(%rbp) # p = rax (在栈上)
movq -8(%rbp), %rax # rax = p
movl $42, %edi # 第一个参数放入 edi
call *%rax # 间接调用:call hello的地址
# hello:
movl %edi, %esi # 参数从 edi 移到 esi(printf 第二个参数)
leaq .LC0(%rip), %rdi # 格式化字符串
call printf@PLT
ret
关键指令:call *%rax —— 从寄存器取地址跳转,这就是"间接调用"。
对比:普通调用 vs 间接调用
hello(42); // call hello — 编译时决定地址
p(42); // call *%rax — 运行时决定地址(多一层间接)
间接调用的代价:一次额外的内存读取(从变量或内存中加载地址)。
为什么函数代码不会被释放
程序地址空间:
┌──────────────────────────────────┐
│ .text 段(只读) │
│ 0x401000: hello 的机器码 │ ← p 指向这里
│ 0x401050: world 的机器码 │
│ 0x401100: main 的机器码 │
├──────────────────────────────────┤
│ .data / .bss 段(读写) │
│ 0x502000: p 变量(存 0x401000) │
└──────────────────────────────────┘
.text 段在进程退出前永远不会被释放。所以函数指针永远有效——你用函数名取的地址在 .text 段。这和局部变量的栈分配完全是两套机制。
7. 进阶:闭包模拟
C 没有闭包,但可以用结构体 + 函数指针模拟:
/* 定义一个"对象":数据 + 方法 */
typedef struct {
int count;
void (*on_event)(struct counter *self, int n);
} counter_t;
/* 方法实现 */
static void counter_handler(counter_t *self, int n)
{
self->count += n;
printf("count = %d\n", self->count);
}
/* 初始化 */
void counter_init(counter_t *c)
{
c->count = 0;
c->on_event = counter_handler; /* 绑定方法 */
}
/* 使用 */
counter_t c;
counter_init(&c);
c.on_event(&c, 5); /* 输出 count = 5 */
c.on_event(&c, 3); /* 输出 count = 8 */
这就是 C 里模拟"面向对象"的典型手法——结构体存数据和函数指针,函数指针第一个参数传结构体自身(类似 C++ 的 this)。
8. 练习
下面几个例子你在项目里跑一遍,理解回调的工作方式:
练习 1:手写一个回调注册 + 触发
#include <stdio.h>
typedef void (*cb_t)(int);
static cb_t g_cb;
void reg(cb_t cb) {
g_cb = cb;
}
void fire(int n) {
if (g_cb) g_cb(n);
}
static void my(int n) {
printf("n=%d\n", n);
}
int main(void) {
reg(my);
fire(100); /* 期望输出 n=100 */
return 0;
}
练习 2:带上下文
struct ctx {
int id;
};
typedef void (*cb_t)(int code, void *ctx);
/* 自行实现 reg / fire / handler / main */
/* 要求 handler 能打印 ctx->id */
练习 3:读懂本项目现有的回调链
// 1. hal/btn-input.h 声明类型 btn_longpress_handler_t
// 2. main.c 调用 btn_input_set_longpress(3000, on_power_longpress)
// 3. indev.c 的 process_events() 在计时到 3s 时触发回调
// 4. on_power_longpress(115) 被调用
总结
| 概念 | 一句话 |
|---|---|
| 函数名 | 就是地址,func 和 &func 一样 |
| 函数指针 | void (*p)(int) —— p 存的是函数地址 |
| typedef | typedef void (*F)(int) —— 把类型换成名字 |
| 回调 | 把函数地址传给别人,别人到时候调回来 |
| 上下文指针 | 传一个 void * 给回调,让它能访问外部数据 |
| .text 段 | 存代码,只读,程序退出前不释放 |
| 间接调用 | call *%rax —— 比普通 call 多一次内存读取 |