函数指针进阶:函数指针数组、回调函数机制、函数指针作为结构体成员
好,咱们继续往下挖。函数指针这东西,光会声明和调用,说实话只是入门。真正让它发光发热的,是今天要聊的这三个进阶玩法:函数指针数组、回调函数机制、以及函数指针作为结构体成员。我在嵌入式项目里,这三个东西几乎天天见。
一、函数指针数组:把函数当表格用
你想想看,如果有一组功能相似的函数,比如不同的加密算法、不同的滤波函数、不同的命令处理函数。你难道要写一长串 if-else 或者 switch-case 去调用它们吗?
我个人习惯用函数指针数组。说白了,就是把函数指针塞进一个数组里,用索引直接跳转调用。
#include <stdio.h>
// 定义几个处理函数
void process_a(int x) {
printf("Process A: %d\n", x * 2);
}
void process_b(int x) {
printf("Process B: %d\n", x + 10);
}
void process_c(int x) {
printf("Process C: %d\n", x - 3);
}
int main() {
// 声明函数指针数组
void (*processors[3])(int) = {process_a, process_b, process_c};
int choice = 1; // 假设从用户输入或协议中拿到这个值
if (choice >= 0 && choice < 3) {
processors[choice](42);
}
return 0;
}
我在项目中遇到过类似场景:一个串口命令解析器,收到不同的命令码,就调用不同的处理函数。用函数指针数组,代码直接从 O(n) 的 if-else 变成了 O(1) 的查表跳转。效率高,而且扩展性好——加一个新命令,只需要在数组里加一项,不用改核心逻辑。
返回值类型 (*数组名[元素个数])(参数列表),注意括号的位置,别写错了。
typedef void (*Processor)(int); 然后 Processor processors[3]; 这样读起来舒服多了。
二、回调函数机制:把控制权交出去
回调函数,说白了就是「你调用我提供的函数,但具体做什么由你决定」。这是实现解耦的利器。
标准库里的 qsort 就是典型例子。它不知道你要排序什么类型的数据,也不知道按什么规则排序。所以它让你提供一个比较函数,它只管调用。
#include <stdio.h>
#include <stdlib.h>
// 比较函数:升序
int cmp_asc(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
// 比较函数:降序
int cmp_desc(const void *a, const void *b) {
return *(int*)b - *(int*)a;
}
int main() {
int arr[] = {5, 2, 8, 1, 9};
int n = sizeof(arr) / sizeof(arr[0]);
// 传入不同的回调,实现不同排序
qsort(arr, n, sizeof(int), cmp_asc);
// 或者 qsort(arr, n, sizeof(int), cmp_desc);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
我曾经写过一个传感器驱动库,不同型号的传感器读取数据的方式不同。我就定义了一个回调函数指针,让上层应用注册自己的读取函数。底层驱动只负责调度和数据处理,完全不关心数据怎么来的。嗯,这就是回调的魅力。
三、函数指针作为结构体成员:面向对象风格的C
C语言没有类,但我们可以用结构体+函数指针模拟出对象的行为。这在嵌入式驱动开发中非常常见。
#include <stdio.h>
// 定义一个"设备"结构体
typedef struct {
int id;
void (*init)(void);
int (*read)(void);
void (*write)(int data);
} Device;
// 具体实现
void uart_init(void) {
printf("UART initialized.\n");
}
int uart_read(void) {
printf("UART read.\n");
return 0x55;
}
void uart_write(int data) {
printf("UART write: 0x%02X\n", data);
}
int main() {
Device uart_device = {
.id = 1,
.init = uart_init,
.read = uart_read,
.write = uart_write
};
// 像操作对象一样使用
uart_device.init();
int val = uart_device.read();
uart_device.write(val);
return 0;
}
我在项目中用这种模式写过 SPI、I2C、GPIO 的抽象层。每个外设都是一个结构体实例,里面有 init、read、write、ioctl 等函数指针。上层代码完全不用关心底层是哪个硬件,只要调用统一的接口就行。换硬件?换个结构体实例就好了。
四、知识体系总览
下面这张图,把今天讲的三个知识点串起来了。你可以看到它们各自的应用场景和相互关系。
五、避坑指南与经验总结
讲了这么多,最后分享几个我实际踩过的坑:
- 我曾经在函数指针数组里忘记检查索引边界,结果数组越界,程序直接跑飞。后来我养成了习惯:每次访问前先判断索引是否在有效范围内。
- 我曾经在回调函数里修改了全局状态,导致多线程环境下数据竞争。记住:回调函数尽量设计成无状态的,或者用参数传递上下文。
- 我曾经把函数指针作为结构体成员,但忘了在初始化时赋值。调用时直接段错误。建议用宏或者工厂函数来保证每个成员都被正确初始化。
好了,函数指针的进阶用法就聊到这儿。这些东西看着简单,但用好了,能让你的C代码上一个台阶。下次写驱动或者中间件的时候,不妨试试这些手法。