2. 函数指针作为参数:将函数指针传递给另一个函数,实现策略模式
好,咱们接着聊。上一章我们搞清楚了函数指针怎么定义、怎么赋值、怎么调用。说白了,就是搞明白了「函数指针是什么」。
那这一章,我们来玩点更实用的——把函数指针当作参数,传给另一个函数。
你想想看,如果函数指针能当参数传,那调用方就能决定「被调函数里到底执行哪一段逻辑」。这不就是传说中的策略模式吗?
我在项目中遇到过好几次这样的场景:一个排序函数,今天想按升序排,明天想按降序排,后天又想按自定义规则排。难道每次都要改排序函数的源码?太蠢了。正确的做法是——把比较逻辑作为函数指针传进去。
2.1 为什么要把函数指针当参数传?
先问一个问题:你写一个函数,能不能让调用者决定「某一步具体怎么做」?
常规做法是写死逻辑。比如:
void process_data(int *data, int len) {
for (int i = 0; i < len; i++) {
data[i] = data[i] * 2; // 写死了:乘以2
}
}
如果下次想「加10」呢?再写一个函数?那「先乘2再加10」呢?再再写一个?
你发现没有,这种写法把「做什么」和「怎么做」混在一起了。耦合度太高,扩展性太差。
函数指针作为参数,就是来解决这个问题的。
核心思想:把「具体操作」抽象成一个函数指针参数。调用者传什么函数进来,就执行什么操作。
2.2 一个最简单的例子:数值变换器
我们先写一个极简的例子,让你感受一下「传函数指针」到底长什么样。
#include <stdio.h>
// 定义两个具体的操作函数
int double_it(int x) {
return x * 2;
}
int add_ten(int x) {
return x + 10;
}
// 接收函数指针作为参数的函数
void transform(int *arr, int len, int (*op)(int)) {
for (int i = 0; i < len; i++) {
arr[i] = op(arr[i]); // 调用传入的函数
}
}
int main() {
int nums[] = {1, 2, 3, 4, 5};
int len = sizeof(nums) / sizeof(nums[0]);
// 传入 double_it 函数
transform(nums, len, double_it);
// 结果:2, 4, 6, 8, 10
// 传入 add_ten 函数
transform(nums, len, add_ten);
// 结果:12, 14, 16, 18, 20
return 0;
}
你看,transform 函数根本不知道外面传进来的是什么操作。它只管「对每个元素调用一下传进来的函数」。具体怎么变换,由调用者决定。
这就是策略模式的雏形。
2.3 实战:用函数指针实现可配置的排序
好,光看数值变换不过瘾。咱们来点真正有工程意义的——可配置的排序函数。
我记得有一次做嵌入式数据采集项目,需要把传感器数据按不同规则排序。有时候按时间戳升序,有时候按数值降序,有时候按优先级。如果每种排序都写一个函数,那代码量就爆炸了。
正确的做法是这样:
#include <stdio.h>
#include <stdlib.h>
// 比较函数类型:接收两个 int,返回正数、负数或0
typedef int (*CompareFunc)(int, int);
// 升序比较
int asc(int a, int b) {
return a - b;
}
// 降序比较
int desc(int a, int b) {
return b - a;
}
// 按绝对值升序
int abs_asc(int a, int b) {
return abs(a) - abs(b);
}
// 通用排序函数:用冒泡排序演示
void sort(int *arr, int len, CompareFunc cmp) {
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - 1 - i; j++) {
if (cmp(arr[j], arr[j+1]) > 0) { // 用传入的比较函数
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
void print_arr(int *arr, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int data[] = {3, -1, 5, -7, 2, -4};
int len = sizeof(data) / sizeof(data[0]);
sort(data, len, asc);
print_arr(data, len); // -7 -4 -1 2 3 5
sort(data, len, desc);
print_arr(data, len); // 5 3 2 -1 -4 -7
sort(data, len, abs_asc);
print_arr(data, len); // -1 2 3 -4 5 -7
return 0;
}
个人习惯:我一般会用 typedef 给函数指针类型起个别名,比如 typedef int (*CompareFunc)(int, int);。这样函数签名看起来清爽很多,调用方也一目了然。
2.4 策略模式的本质:分离「不变」与「变」
你发现没有,上面这个排序例子中:
- 不变的部分:排序的流程(冒泡排序的循环、交换逻辑)
- 变化的部分:比较的规则(升序、降序、绝对值排序)
函数指针作为参数,就是把「变化的部分」抽出来,交给调用者去决定。这就是策略模式的核心。
我曾经在一个通信协议栈里用过这个思路。协议解析时,不同的消息类型需要不同的处理函数。我写了一个通用的消息分发函数,接收一个函数指针数组作为参数。每种消息类型对应一个处理函数。新加一种消息类型?不用改分发函数,只需要在外面注册一个新的处理函数就行。
嗯,这种设计让代码的扩展性好了不止一个档次。
2.5 避坑指南:函数指针作为参数的常见陷阱
讲到这里,我得提醒你几个容易踩的坑。这些都是我当年亲手踩过的。
我曾经犯过的错:
- 类型不匹配:函数指针的参数类型、返回值类型必须完全一致。我曾经把
int (*)(int, int)传给了期望int (*)(int)的参数,编译器报了一堆警告,我还没当回事,结果运行时数据全乱了。 - 传了 NULL 函数指针:如果调用方传了 NULL,而你的函数里直接调用它,那就是段错误。我建议在函数入口处加个断言或判空:
if (cmp == NULL) return; - 回调函数中访问了已释放的内存:如果回调函数里用到了外部变量,要确保这些变量在回调执行时仍然有效。这个在后续章节讲回调机制时会详细说。
2.6 知识结构图
下面这张图,帮你理清这一章的核心逻辑:
2.7 小结
这一章我们干了三件事:
- 理解了为什么需要把函数指针当参数传——为了把「做什么」和「怎么做」分开。
- 看了一个数值变换的例子——最简单的函数指针传参演示。
- 实现了一个可配置的排序函数——这才是工程中真正实用的写法。
说白了,函数指针作为参数,就是C语言里实现策略模式最直接的方式。你不需要设计模式的书,不需要虚函数表,一个函数指针参数就搞定了。
下一章,我们会把这个思路再推进一步——回调函数。到时候你会看到,函数指针作为参数在异步处理、事件驱动编程中能发挥多大的威力。