11、函数指针:声明与使用,回调函数机制

函数指针,说白了就是指向函数的指针。很多初学者一听到「指针」就头大,再听到「函数指针」更是直接懵了。但我想告诉你,这东西在实际项目中太常用了,尤其是做嵌入式开发,你几乎天天跟它打交道。

我个人习惯把函数指针理解成「函数的门牌号」。你不需要把整栋楼搬过去,只需要记住门牌号,就能找到那个函数,然后调用它。嗯,就是这么回事。

11.1 函数指针的声明

先看一个最简单的例子。假设你有一个函数:

int add(int a, int b) {
    return a + b;
}

这个函数的类型是 int (int, int)。那么指向它的指针应该怎么声明?

int (*p)(int, int);  // 声明一个函数指针
p = add;             // 让 p 指向 add 函数

注意括号的位置。写成 int *p(int, int) 就变成了「返回 int* 的函数」,完全不是一回事。我在项目中见过不少同事栽在这个括号上,编译报错半天找不到原因。

声明格式:

返回类型 (*指针变量名)(参数类型列表);

括号不能省,星号必须跟指针变量名在一起。

11.2 函数指针的使用

声明完了怎么用?两种方式:

int result1 = p(3, 5);      // 直接调用
int result2 = (*p)(3, 5);   // 解引用后调用(老派写法)

我个人更喜欢第一种写法,简洁明了。第二种写法是 C 语言早期遗留下来的风格,现在基本没人这么写了。你想想看,既然 p 已经指向了 add,那 p(3, 5) 不就是调用 add(3, 5) 吗?

来看一个完整的例子:

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    int (*op)(int, int);  // 声明函数指针
    
    op = add;
    printf("add: %d\n", op(10, 5));  // 输出 15
    
    op = sub;
    printf("sub: %d\n", op(10, 5));  // 输出 5
    
    return 0;
}

你看,同一个指针变量,可以指向不同的函数。这就是函数指针的灵活性所在。

11.3 回调函数机制

回调函数,是函数指针最经典的应用场景。什么叫回调?说白了就是:你调用一个函数,同时告诉它「完事之后叫我一声」,这个「叫我一声」就是回调。

我在做嵌入式项目时,经常用回调来处理中断。比如定时器溢出后,自动调用你注册的处理函数。这样主程序就不用一直轮询等待了。

来看一个标准库中的经典例子——qsort

#include <stdio.h>
#include <stdlib.h>

int compare(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    qsort(arr, n, sizeof(int), compare);  // compare 就是回调函数
    
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

qsort 不知道你要怎么比较元素,它把这个决定权交给你。你写一个比较函数,把函数指针传进去,qsort 在需要比较的时候就会回调你的函数。这就是回调机制的核心思想——把行为参数化

我的经验:回调函数在驱动层、协议栈、GUI 框架中随处可见。比如你注册一个按键按下回调,系统检测到按键事件后自动调用你的处理逻辑。这样代码的耦合度极低,扩展性极好。

11.4 函数指针数组

有时候你需要管理一组函数,比如一个计算器程序:

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 div(int a, int b) { return b ? a / b : 0; }

int main() {
    int (*ops[4])(int, int) = {add, sub, mul, div};
    char op_chars[] = {'+', '-', '*', '/'};
    
    for (int i = 0; i < 4; i++) {
        printf("10 %c 5 = %d\n", op_chars[i], ops[i](10, 5));
    }
    return 0;
}

函数指针数组的声明看起来有点吓人:int (*ops[4])(int, int)。但拆开看就简单了:ops[4] 是一个数组,每个元素是 int (*)(int, int) 类型的函数指针。我在项目中用这种方式实现过状态机,每个状态对应一个处理函数,用数组索引直接跳转,效率非常高。

11.5 函数指针作为结构体成员

把函数指针塞进结构体,就模拟出了「面向对象」的味道。这在嵌入式驱动中非常常见:

typedef struct {
    void (*init)(void);
    void (*send)(const char *data);
    char* (*receive)(void);
} UART_Driver;

void uart1_init(void) { /* 初始化 UART1 */ }
void uart1_send(const char *data) { /* 发送数据 */ }
char* uart1_receive(void) { /* 接收数据 */ }

int main() {
    UART_Driver uart1 = {
        .init = uart1_init,
        .send = uart1_send,
        .receive = uart1_receive
    };
    
    uart1.init();
    uart1.send("Hello");
    return 0;
}

你看,通过结构体把一组相关的操作打包在一起,调用时只需要操作这个结构体变量。我曾经用这种模式写过 SPI、I2C、GPIO 的驱动层,上层代码完全不用关心底层硬件差异,换芯片只需要换一套函数指针赋值就行。

避坑指南:我曾经在回调函数中不小心传入了野指针,结果程序跑飞,查了两天才找到原因。使用函数指针时,一定要确保:

  • 指针已经被正确赋值,不是 NULL
  • 回调函数的参数和返回值类型完全匹配
  • 回调函数内部不要使用已经释放的资源

11.6 知识结构图

下面这张图帮你理清函数指针的核心脉络:

函数指针 声明与语法 返回类型 (*ptr)(参数列表) 调用方式 ptr(args) 或 (*ptr)(args) 回调函数 行为参数化,解耦调用方 高级应用 函数指针数组、结构体成员 核心价值:将函数作为参数传递,实现灵活扩展 驱动层、协议栈、状态机、排序算法中广泛使用

11.7 总结

函数指针不是什么玄学,它就是函数的地址。你掌握了它,就能写出更灵活、更解耦的代码。回调机制让调用方和被调用方之间只通过一个指针约定接口,双方互不依赖。

我在实际项目中,几乎每个模块都会用到函数指针。从按键扫描到通信协议解析,从定时器回调到任务调度,它无处不在。你刚开始可能觉得别扭,但用上几次就会发现——这东西真香。

一句话记住:函数指针 = 函数的门牌号。你不需要知道函数内部怎么实现的,只要知道门牌号,就能找到它、调用它。


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