1. 函数指针基础:定义、声明与初始化
说实话,很多C语言开发者学了几年,指针用得挺溜,但一碰到函数指针就犯怵。我当年刚入行时也是这样——总觉得函数指针这玩意儿有点玄乎。直到有一次在项目中调试一个通信协议栈,被逼着啃了三天函数指针的文档,才真正开了窍。
函数指针是什么?说白了,它就是指向函数的指针。普通指针存的是变量的地址,函数指针存的是函数的入口地址。你想想看,既然变量地址能存起来、能传递,那函数地址为什么不行?
1.1 函数指针的定义与声明
先看一个最基础的例子。假设我们有一个加法函数:
int add(int a, int b) {
return a + b;
}
要定义一个指向这种函数的指针,写法是这样的:
int (*func_ptr)(int, int);
嗯,这里要注意括号的位置。*func_ptr 必须用括号括起来。如果写成 int *func_ptr(int, int),那就变成了一个返回 int* 的函数声明,完全不是一回事。我见过不少新手在这里栽跟头。
声明完成后,就可以初始化了:
func_ptr = add; // 方式一:直接赋值函数名
func_ptr = &add; // 方式二:取地址符(其实和方式一等效)
我个人习惯用方式一,简洁明了。函数名本身就是一个地址常量,就像数组名一样。
1.2 调用函数指针
初始化之后怎么用?两种写法都行:
int result1 = func_ptr(3, 5); // 方式一:直接调用
int result2 = (*func_ptr)(3, 5); // 方式二:解引用后调用
两种方式结果一样。我更喜欢第一种,看起来更自然。但有些老派的C程序员坚持用第二种,觉得这样才体现了「指针」的本质。这个看个人喜好,没有对错。
1.3 使用 typedef 简化类型定义
每次写 int (*)(int, int) 这种类型,手指都累。这时候 typedef 就派上用场了:
typedef int (*MathOp)(int, int);
这行代码的意思是:定义了一个类型 MathOp,它是一个指向「接受两个int参数、返回int」的函数的指针类型。之后声明变量就简单了:
MathOp op_ptr = add;
int result = op_ptr(10, 20); // result = 30
我在项目中维护过一个计算器模块,里面定义了七八种运算函数。用 typedef 之后,代码清晰了很多。你想想看,如果不用 typedef,每次声明都要写一长串类型,多痛苦。
typedef int (*FuncType)(int, int)。这个写法虽然有点反直觉,但记住就行——这是C语言的语法规则。
1.4 函数指针作为函数参数
这才是函数指针真正发光的地方。把函数指针当作参数传给另一个函数,就实现了回调机制。
举个实际例子。我在做嵌入式传感器驱动时,经常需要遍历一个数组,对每个元素做不同的处理。用回调函数就非常灵活:
// 定义一个处理函数类型
typedef int (*Processor)(int);
// 遍历数组,对每个元素应用回调
void process_array(int *arr, int size, Processor proc) {
for (int i = 0; i < size; i++) {
arr[i] = proc(arr[i]);
}
}
// 具体的处理函数
int double_it(int x) { return x * 2; }
int square_it(int x) { return x * x; }
// 使用
int data[] = {1, 2, 3, 4, 5};
process_array(data, 5, double_it); // 每个元素翻倍
process_array(data, 5, square_it); // 每个元素平方
你看,同一个 process_array 函数,传入不同的回调,行为就完全不同。这就是回调机制的核心思想——把行为参数化。
1.5 函数指针数组
有时候我们需要管理一组函数指针。比如一个菜单系统,每个菜单项对应一个处理函数:
typedef void (*MenuHandler)(void);
void open_file(void) { /* ... */ }
void save_file(void) { /* ... */ }
void exit_app(void) { /* ... */ }
MenuHandler menu_actions[] = {
open_file,
save_file,
exit_app
};
// 根据菜单索引调用
int choice = get_user_choice(); // 假设返回 0, 1, 2
if (choice >= 0 && choice < 3) {
menu_actions[choice]();
}
这种写法在状态机、命令模式中非常常见。我做过一个物联网网关项目,设备上报的不同消息类型就对应不同的处理函数,用一个函数指针数组搞定,比写一长串 switch-case 优雅多了。
1.6 常见陷阱与避坑指南
讲几个我踩过的坑:
- 函数指针类型不匹配: 回调函数的参数个数、类型、返回值必须完全一致。我曾经把
int (*)(int)赋值给int (*)(int, int)的指针,编译没报错,运行时数据全乱了。 - 空指针调用: 调用函数指针前一定要判空。特别是在回调注册机制中,用户可能没注册回调就直接调用了。
- 函数指针的作用域: 如果函数指针指向的是局部函数(比如内部函数或静态函数),要确保该函数在调用时仍然有效。
1.7 本章知识结构图
下面这张图梳理了函数指针的核心知识点,我建议你把它存下来,复习时对照着看:
这张图把函数指针的五个核心知识点串起来了。从定义声明开始,到初始化、typedef 简化、作为参数传递,再到函数指针数组,每一步都是下一层的基础。我建议你按照这个顺序去理解和练习。
好了,函数指针的基础就讲到这里。记住一句话:函数指针不是魔法,它只是C语言给我们的一个工具——把函数当作「一等公民」来对待。下一节我们会深入回调机制的实际应用,到时候你会看到函数指针的真正威力。