6、函数指针:函数指针的声明与使用,回调函数的实现机制,函数指针数组
说实话,很多C语言学习者学到指针就开始头疼。等学到函数指针,那更是直接想放弃。我当年刚入行时也一样,觉得“函数还能有指针?函数不就是一个名字吗?”
后来做嵌入式项目,才真正体会到函数指针的威力。说白了,函数指针就是指向函数的指针变量。它存的是函数的入口地址。你想想看,变量有地址,数组有地址,函数当然也有地址——它在内存中就是一段机器码,自然有个起始位置。
6.1 函数指针的声明
声明一个函数指针,语法上有点绕。我个人的记忆方法是:先写出函数的原型,然后把函数名换成 (*指针名)。
举个例子,假设有个函数:
int add(int a, int b) {
return a + b;
}
那么指向它的函数指针就是:
int (*func_ptr)(int, int);
注意括号不能少。如果写成 int *func_ptr(int, int),那编译器会认为你声明了一个返回 int* 的函数,而不是一个指针。
核心记忆口诀:
int (*p)(int, int)—— p 是一个指针,指向一个返回 int、参数为两个 int 的函数int *p(int, int)—— p 是一个函数,返回 int*
我在项目中遇到过有人把括号写错,结果编译报错半天找不出原因。嗯,这种细节真的要注意。
6.2 函数指针的使用
赋值和调用都很直接:
int (*func_ptr)(int, int) = &add; // 取地址符可省略
int result = func_ptr(3, 5); // 直接调用
// 或者
int result = (*func_ptr)(3, 5); // 解引用后调用
我个人习惯用第一种写法,简洁明了。你想想看,函数名本身就是一个地址,所以 &add 和 add 是一样的。
小技巧:在嵌入式系统中,函数指针常用于实现跳转表或状态机。我曾经用函数指针数组实现过一个按键处理模块,代码量减少了40%。
6.3 回调函数的实现机制
回调函数,说白了就是把函数指针当作参数传给另一个函数。这样,被调用的函数可以在适当的时候“回调”你传进去的函数。
来看一个经典的例子——排序:
#include <stdio.h>
// 比较函数
int ascending(int a, int b) {
return a > b;
}
int descending(int a, int b) {
return a < b;
}
// 排序函数,接受回调
void bubble_sort(int arr[], int n, int (*compare)(int, int)) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (compare(arr[j], arr[j + 1])) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[] = {5, 3, 8, 1, 2};
int n = 5;
bubble_sort(arr, n, ascending); // 升序
// bubble_sort(arr, n, descending); // 降序
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
你看,同一个排序函数,传入不同的回调,行为就完全不一样。这就是回调的威力——把行为参数化。
注意:回调函数不能阻塞太久。我在做实时系统时,曾经因为回调里做了延时操作,导致整个系统响应变慢。回调应该是轻量级的,如果需要耗时操作,建议用任务队列异步处理。
6.4 函数指针数组
函数指针数组,就是把多个函数指针放在一个数组里。这在实现菜单系统、命令解析器时特别有用。
声明方式:
int (*func_array[])(int, int) = {
add,
subtract,
multiply,
divide
};
调用时:
int result = func_array[op_index](a, b);
我曾经用这个方式实现过一个计算器,代码结构非常清晰。每个运算函数独立维护,新增运算只需要加一个函数和数组元素就行。
实际项目中的应用:
- 命令解析器:根据命令字符串索引到对应的处理函数
- 状态机:每个状态对应一个处理函数,状态切换就是切换函数指针
- 协议栈:不同消息类型对应不同的处理回调
6.5 知识体系总览
下面这张图,是我梳理的函数指针知识结构。你看一眼就能明白整个脉络:
6.6 避坑指南
最后,分享几个我踩过的坑:
- 类型不匹配:回调函数的参数类型和个数必须完全一致,否则编译器不会报错,但运行时可能崩溃。我曾经因为一个
int和long的差异,查了整整一个下午。 - 空指针检查:调用函数指针前,一定要检查是否为 NULL。特别是在嵌入式系统中,一个空指针调用直接就是 HardFault。
- 作用域问题:如果回调函数是某个模块内部的静态函数,外部无法直接访问。这时候需要用
extern或者通过注册接口暴露。
我的习惯:在项目里,我会用 typedef 给函数指针起个名字,这样代码可读性会好很多:
typedef int (*CompareFunc)(int, int);
void sort(int arr[], int n, CompareFunc cmp);
你看,这样声明是不是清爽多了?
函数指针这东西,刚开始觉得绕,用多了就会发现它其实很自然。说白了,它就是C语言里实现“多态”和“回调”的基础。你想想看,没有函数指针,很多设计模式根本玩不转。