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);     // 解引用后调用

我个人习惯用第一种写法,简洁明了。你想想看,函数名本身就是一个地址,所以 &addadd 是一样的。

小技巧:在嵌入式系统中,函数指针常用于实现跳转表或状态机。我曾经用函数指针数组实现过一个按键处理模块,代码量减少了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 知识体系总览

下面这张图,是我梳理的函数指针知识结构。你看一眼就能明白整个脉络:

函数指针知识体系 函数指针 声明与使用 回调函数机制 函数指针数组 声明语法 赋值与调用 typedef 简化 函数作为参数 行为参数化 轻量级设计 声明与初始化 索引调用 命令解析器 应用场景:菜单系统 / 状态机 / 协议栈 / 计算器

6.6 避坑指南

最后,分享几个我踩过的坑:

  • 类型不匹配:回调函数的参数类型和个数必须完全一致,否则编译器不会报错,但运行时可能崩溃。我曾经因为一个 intlong 的差异,查了整整一个下午。
  • 空指针检查:调用函数指针前,一定要检查是否为 NULL。特别是在嵌入式系统中,一个空指针调用直接就是 HardFault。
  • 作用域问题:如果回调函数是某个模块内部的静态函数,外部无法直接访问。这时候需要用 extern 或者通过注册接口暴露。

我的习惯:在项目里,我会用 typedef 给函数指针起个名字,这样代码可读性会好很多:

typedef int (*CompareFunc)(int, int);
void sort(int arr[], int n, CompareFunc cmp);

你看,这样声明是不是清爽多了?

函数指针这东西,刚开始觉得绕,用多了就会发现它其实很自然。说白了,它就是C语言里实现“多态”和“回调”的基础。你想想看,没有函数指针,很多设计模式根本玩不转。


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