11、函数指针:声明与使用,回调函数机制
函数指针,说白了就是指向函数的指针。很多初学者一听到「指针」就头大,再听到「函数指针」更是直接懵了。但我想告诉你,这东西在实际项目中太常用了,尤其是做嵌入式开发,你几乎天天跟它打交道。
我个人习惯把函数指针理解成「函数的门牌号」。你不需要把整栋楼搬过去,只需要记住门牌号,就能找到那个函数,然后调用它。嗯,就是这么回事。
11.1 函数指针的声明
先看一个最简单的例子。假设你有一个函数:
int add(int a, int b) {
return a + b;
}
这个函数的类型是 int (int, int)。那么指向它的指针应该怎么声明?
int (*p)(int, int); // 声明一个函数指针
p = add; // 让 p 指向 add 函数
注意括号的位置。写成 int *p(int, int) 就变成了「返回 int* 的函数」,完全不是一回事。我在项目中见过不少同事栽在这个括号上,编译报错半天找不到原因。
声明格式:
返回类型 (*指针变量名)(参数类型列表);
括号不能省,星号必须跟指针变量名在一起。
11.2 函数指针的使用
声明完了怎么用?两种方式:
int result1 = p(3, 5); // 直接调用
int result2 = (*p)(3, 5); // 解引用后调用(老派写法)
我个人更喜欢第一种写法,简洁明了。第二种写法是 C 语言早期遗留下来的风格,现在基本没人这么写了。你想想看,既然 p 已经指向了 add,那 p(3, 5) 不就是调用 add(3, 5) 吗?
来看一个完整的例子:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
int (*op)(int, int); // 声明函数指针
op = add;
printf("add: %d\n", op(10, 5)); // 输出 15
op = sub;
printf("sub: %d\n", op(10, 5)); // 输出 5
return 0;
}
你看,同一个指针变量,可以指向不同的函数。这就是函数指针的灵活性所在。
11.3 回调函数机制
回调函数,是函数指针最经典的应用场景。什么叫回调?说白了就是:你调用一个函数,同时告诉它「完事之后叫我一声」,这个「叫我一声」就是回调。
我在做嵌入式项目时,经常用回调来处理中断。比如定时器溢出后,自动调用你注册的处理函数。这样主程序就不用一直轮询等待了。
来看一个标准库中的经典例子——qsort:
#include <stdio.h>
#include <stdlib.h>
int compare(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int main() {
int arr[] = {5, 2, 8, 1, 9, 3};
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), compare); // compare 就是回调函数
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
qsort 不知道你要怎么比较元素,它把这个决定权交给你。你写一个比较函数,把函数指针传进去,qsort 在需要比较的时候就会回调你的函数。这就是回调机制的核心思想——把行为参数化。
我的经验:回调函数在驱动层、协议栈、GUI 框架中随处可见。比如你注册一个按键按下回调,系统检测到按键事件后自动调用你的处理逻辑。这样代码的耦合度极低,扩展性极好。
11.4 函数指针数组
有时候你需要管理一组函数,比如一个计算器程序:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b ? a / b : 0; }
int main() {
int (*ops[4])(int, int) = {add, sub, mul, div};
char op_chars[] = {'+', '-', '*', '/'};
for (int i = 0; i < 4; i++) {
printf("10 %c 5 = %d\n", op_chars[i], ops[i](10, 5));
}
return 0;
}
函数指针数组的声明看起来有点吓人:int (*ops[4])(int, int)。但拆开看就简单了:ops[4] 是一个数组,每个元素是 int (*)(int, int) 类型的函数指针。我在项目中用这种方式实现过状态机,每个状态对应一个处理函数,用数组索引直接跳转,效率非常高。
11.5 函数指针作为结构体成员
把函数指针塞进结构体,就模拟出了「面向对象」的味道。这在嵌入式驱动中非常常见:
typedef struct {
void (*init)(void);
void (*send)(const char *data);
char* (*receive)(void);
} UART_Driver;
void uart1_init(void) { /* 初始化 UART1 */ }
void uart1_send(const char *data) { /* 发送数据 */ }
char* uart1_receive(void) { /* 接收数据 */ }
int main() {
UART_Driver uart1 = {
.init = uart1_init,
.send = uart1_send,
.receive = uart1_receive
};
uart1.init();
uart1.send("Hello");
return 0;
}
你看,通过结构体把一组相关的操作打包在一起,调用时只需要操作这个结构体变量。我曾经用这种模式写过 SPI、I2C、GPIO 的驱动层,上层代码完全不用关心底层硬件差异,换芯片只需要换一套函数指针赋值就行。
避坑指南:我曾经在回调函数中不小心传入了野指针,结果程序跑飞,查了两天才找到原因。使用函数指针时,一定要确保:
- 指针已经被正确赋值,不是 NULL
- 回调函数的参数和返回值类型完全匹配
- 回调函数内部不要使用已经释放的资源
11.6 知识结构图
下面这张图帮你理清函数指针的核心脉络:
11.7 总结
函数指针不是什么玄学,它就是函数的地址。你掌握了它,就能写出更灵活、更解耦的代码。回调机制让调用方和被调用方之间只通过一个指针约定接口,双方互不依赖。
我在实际项目中,几乎每个模块都会用到函数指针。从按键扫描到通信协议解析,从定时器回调到任务调度,它无处不在。你刚开始可能觉得别扭,但用上几次就会发现——这东西真香。
一句话记住:函数指针 = 函数的门牌号。你不需要知道函数内部怎么实现的,只要知道门牌号,就能找到它、调用它。
公众号:蓝海资料掘金营,微信deep3321