第12章 函数指针:声明、赋值、数组与回调
函数指针,说白了就是指向函数的指针。很多初学者一听到「函数指针」四个字就头大,觉得这玩意儿抽象得不行。我当年刚学的时候也懵,心想函数不就是一段代码吗?怎么还能有地址?
嗯,其实你想想看,程序编译之后,函数就变成了一段机器码,存放在内存的代码段里。既然是内存里的东西,那它当然有地址。函数指针,就是用来存这个地址的变量。
这一章,咱们就把函数指针的声明、赋值、调用、数组,还有回调函数,一个一个掰开揉碎了讲清楚。
12.1 函数指针的声明
函数指针的声明,语法上有点绕。我个人的记忆方法是:先写一个普通函数声明,然后把函数名换成 (*指针名)。
举个例子,假设你有一个函数:
int add(int a, int b);
那么对应的函数指针声明就是:
int (*pFunc)(int, int);
注意括号不能少。如果写成 int *pFunc(int, int),那编译器会认为你声明了一个返回 int* 的函数,而不是一个函数指针。
核心语法:
返回类型 (*指针变量名)(参数类型列表);
我在项目中见过有人把函数指针声明写成这样:
void (*)(int) pFunc; // 错误!
这是不对的。正确的写法是:
void (*pFunc)(int);
嗯,这里要注意:指针变量名必须紧跟在 * 后面,用括号括起来。
12.2 函数指针的赋值与调用
声明完了,怎么赋值?很简单,直接把函数名赋给指针就行。函数名本身就代表函数的入口地址。
int add(int a, int b) {
return a + b;
}
int main() {
int (*pFunc)(int, int); // 声明函数指针
pFunc = add; // 赋值,也可以写成 pFunc = &add;
int result = pFunc(3, 5); // 调用方式一:直接通过指针调用
// int result = (*pFunc)(3, 5); // 调用方式二:解引用后调用
printf("result = %d\n", result);
return 0;
}
两种调用方式都可以。我个人习惯用第一种,写起来简洁。不过有些老派的 C 程序员喜欢用第二种,觉得那样更能体现「解引用」的语义。都行,看个人喜好。
小技巧:用 typedef 给函数指针起个别名,代码会清爽很多。
typedef int (*MathOp)(int, int);
MathOp pFunc = add;
我曾经在一个嵌入式项目里,用函数指针实现了菜单系统。每个菜单项对应一个函数指针,用户按上下键切换菜单,按确认键就调用对应的函数。代码结构非常清晰,后来维护起来也省了不少事。
12.3 函数指针数组
函数指针数组,就是把多个函数指针放在一个数组里。这东西在实现状态机、命令分发器的时候特别好用。
声明语法:
int (*pFuncArray[3])(int, int);
这行代码声明了一个数组,数组有 3 个元素,每个元素都是一个函数指针,指向返回 int、接受两个 int 参数的函数。
来个完整的例子:
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 main() {
int (*ops[3])(int, int) = {add, sub, mul};
int a = 10, b = 5;
for (int i = 0; i < 3; i++) {
printf("ops[%d](%d, %d) = %d\n", i, a, b, ops[i](a, b));
}
return 0;
}
输出结果:
ops[0](10, 5) = 15
ops[1](10, 5) = 5
ops[2](10, 5) = 50
你看,用函数指针数组,一个循环就把三个函数都调了一遍。如果不用这个技巧,你得写三个 if-else 或者 switch-case,代码又长又丑。
避坑指南:我曾经在写一个通信协议解析模块时,用函数指针数组来分发不同的消息类型。结果数组下标越界了,程序直接跑飞。调试了半天才发现是消息类型编号从 1 开始,而我数组下标从 0 开始。所以用函数指针数组时,一定要确保下标和函数一一对应,别搞错偏移量。
12.4 回调函数
回调函数,说白了就是「你把一个函数的地址告诉我,我在合适的时机调用它」。这是函数指针最经典的应用场景。
举个例子,标准库里的 qsort 函数就用了回调:
#include <stdlib.h>
int compare(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int main() {
int arr[] = {5, 3, 8, 1, 9};
qsort(arr, 5, sizeof(int), compare);
return 0;
}
qsort 不知道你要怎么比较两个元素,所以它让你自己写一个比较函数,然后把函数指针传给它。这就是回调。
我在嵌入式开发中经常用回调来处理中断。比如一个定时器中断,我不想在中断服务函数里写太多逻辑,就注册一个回调函数,中断发生时调用它。这样主程序和中断逻辑就解耦了,测试也方便。
回调函数的典型应用场景:
- 排序、查找等算法中的比较函数
- 事件驱动编程(如 GUI 按钮点击事件)
- 中断处理、定时器回调
- 异步 I/O 完成通知
12.5 知识体系总览
下面这张图,把函数指针的核心知识点串起来了。你可以把它当作一个快速复习的路线图。
12.6 避坑总结
最后,把我这些年踩过的坑总结一下,你写代码的时候多留个心眼:
| 常见错误 | 说明 | 正确做法 |
|---|---|---|
| 声明时漏掉括号 | int *pFunc(int, int) 会被解析为函数声明 |
int (*pFunc)(int, int) |
| 函数指针数组下标越界 | 数组大小和实际函数数量不匹配 | 用枚举或宏定义来管理下标 |
| 回调函数类型不匹配 | 参数类型或返回值类型不一致 | 严格检查函数签名,用 typedef 统一类型 |
| 函数指针未初始化就调用 | 野指针调用,程序直接崩溃 | 声明时初始化为 NULL,调用前判空 |
我的习惯:每次声明函数指针,我都会顺手写一个 typedef。这样后面用起来方便,而且不容易写错。比如:
typedef void (*EventHandler)(int eventId, void *data);
然后所有事件处理函数都按这个签名来写,清爽又安全。
函数指针这东西,刚开始觉得绕,用多了就会发现它特别灵活。尤其是回调函数,简直是解耦的利器。你想想看,如果没有函数指针,你怎么让一个通用的排序函数支持不同类型的数据?只能写死,或者用宏,那代码得多难看。
好了,这一章的内容就到这儿。函数指针的声明、赋值、调用、数组、回调,这几个点你都搞清楚了,后面遇到复杂一点的场景,比如状态机、插件系统、消息分发,你就能游刃有余了。
公众号:蓝海资料掘金营,微信 deep3321