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 避坑指南:函数指针作为参数的常见陷阱

讲到这里,我得提醒你几个容易踩的坑。这些都是我当年亲手踩过的。

我曾经犯过的错:

  1. 类型不匹配:函数指针的参数类型、返回值类型必须完全一致。我曾经把 int (*)(int, int) 传给了期望 int (*)(int) 的参数,编译器报了一堆警告,我还没当回事,结果运行时数据全乱了。
  2. 传了 NULL 函数指针:如果调用方传了 NULL,而你的函数里直接调用它,那就是段错误。我建议在函数入口处加个断言或判空:if (cmp == NULL) return;
  3. 回调函数中访问了已释放的内存:如果回调函数里用到了外部变量,要确保这些变量在回调执行时仍然有效。这个在后续章节讲回调机制时会详细说。

2.6 知识结构图

下面这张图,帮你理清这一章的核心逻辑:

函数指针作为参数 —— 策略模式核心逻辑 调用方 决定「做什么」 传入函数指针 通用处理函数 执行「怎么做」 调用回调 策略函数 具体实现 策略模式带来的好处 ✅ 解耦:调用方与具体实现解耦 ✅ 扩展:新增策略无需修改通用函数 ✅ 复用:同一个通用函数可搭配任意策略 ✅ 测试:可单独测试每个策略函数 ✅ 灵活:运行时动态切换策略

2.7 小结

这一章我们干了三件事:

  1. 理解了为什么需要把函数指针当参数传——为了把「做什么」和「怎么做」分开。
  2. 看了一个数值变换的例子——最简单的函数指针传参演示。
  3. 实现了一个可配置的排序函数——这才是工程中真正实用的写法。

说白了,函数指针作为参数,就是C语言里实现策略模式最直接的方式。你不需要设计模式的书,不需要虚函数表,一个函数指针参数就搞定了。

下一章,我们会把这个思路再推进一步——回调函数。到时候你会看到,函数指针作为参数在异步处理、事件驱动编程中能发挥多大的威力。


公众号:蓝海资料掘金营,微信 deep3321