前面我们已经分别征服了函数(第十二篇)和指针(第十五、十六篇)。当时我还卖了个关子——函数本身也能被指针指向,而且这种“函数指针”能让你写出高度灵活、可插拔的代码。今天,我们就来把这最后一块拼图补上。
这篇文章会让你理解:如何把函数当作参数传给另一个函数(这正是回调的底层原理),如何用函数指针实现策略模式,以及那个让人头疼的“信号函数”声明到底怎么读。学完之后,你看很多 C 库的源码(比如 qsort、信号处理)就不会再一头雾水。
一、函数也有地址:指向函数的指针
和数组一样,函数在内存中也是一段连续的二进制指令,自然也有起始地址。函数名在表达式中会被转换成这个地址——就像数组名被转换成首元素地址一样。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%p\n", (void*)add); // 打印函数地址
return 0;
}
既然函数有地址,就可以用指针来存它。这就是函数指针。
声明函数指针
函数指针的声明格式,就是把函数声明中的函数名换成 (*指针名):
// 普通函数声明
int add(int a, int b);
// 对应的函数指针声明
int (*func_ptr)(int a, int b); // func_ptr 是指向“返回int、接收两个int”函数的指针
读法:func_ptr 是一个指针,指向一个接收两个 int、返回 int 的函数。
让指针指向函数并调用:
int (*fp)(int, int); // 声明
fp = add; // 赋值(add 和 &add 等价)
// 或 fp = &add;
int result = fp(3, 5); // 通过指针调用,等价于 add(3, 5)
printf("%d\n", result); // 8
函数指针可以像普通函数一样直接加括号调用,fp(3,5) 和 (*fp)(3,5) 都可以。
二、函数指针作为参数:回调函数
函数指针真正的威力在于:把函数当作参数传给另一个函数。这样,被调用的函数可以在适当的时候“回调”你传进去的函数。这在需要通用化操作时极其有用。
回想第十篇,我们写过冒泡排序,但那个排序只能排 int,而且只能升序。如果我想让它能排 double,或者能由用户指定升序还是降序,就要用函数指针。
模拟 qsort 的冒泡排序:
#include <stdio.h>
#include <stdbool.h>
typedef bool (*compare_fn)(int, int); // 用 typedef 简化类型名
bool ascending(int a, int b) { return a > b; } // a>b 说明需要交换
bool descending(int a, int b) { return a < b; }
void bubble_sort(int arr[], int n, compare_fn cmp) {
for (int i = 0; i < n - 1; i++) {
bool swapped = false;
for (int j = 0; j < n - 1 - i; j++) {
if (cmp(arr[j], arr[j+1])) { // 调用回调函数决定是否交换
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = true;
}
}
if (!swapped) break;
}
}
int main(void) {
int arr[] = {5, 2, 8, 1, 9};
int n = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, n, ascending);
printf("升序: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
bubble_sort(arr, n, descending);
printf("降序: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}
输出:
升序: 1 2 5 8 9
降序: 9 8 5 2 1
同一个 bubble_sort,通过传入不同的比较函数,就能改变排序行为。这就是策略模式在 C 语言里的实现——函数指针就是策略的载体。
标准库的 qsort 正是这样做的:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
它使用 void* 实现通用类型,再配合函数指针完成比较操作,是函数指针最经典的应用。
三、typedef 简化函数指针类型
函数指针的语法写起来有点啰嗦,每次写 int (*)(int, int) 容易出错。typedef 是解决这个问题的利器。
typedef int (*operation_fn)(int, int); // operation_fn 是一个类型名
operation_fn fp = add; // 简洁!
这比 int (*fp)(int, int) = add; 好读得多。以后凡是见到 typedef 返回类型 (*类型名)(参数列表); 的写法,就知道是在给函数指针类型取别名。
再看一个更直观的例子——简单计算器:
#include <stdio.h>
typedef int (*calc_fn)(int, int);
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 != 0) ? a / b : 0; }
int calculate(int x, int y, calc_fn op) {
return op(x, y);
}
int main(void) {
printf("10 + 5 = %d\n", calculate(10, 5, add));
printf("10 - 5 = %d\n", calculate(10, 5, sub));
printf("10 * 5 = %d\n", calculate(10, 5, mul));
printf("10 / 5 = %d\n", calculate(10, 5, div));
return 0;
}
calculate 完全不知道具体运算是什么,它只负责在合适的时机调用传入的函数指针。核心逻辑和具体操作彻底解耦。
四、函数指针数组:函数表的雏形
如果有一组同类型的函数,可以放进函数指针数组,用下标来选择执行哪个。这在实现菜单驱动、状态机、命令解析时非常常见。
#include <stdio.h>
typedef void (*command_fn)(void);
void cmd_new(void) { printf("新建文件\n"); }
void cmd_open(void) { printf("打开文件\n"); }
void cmd_save(void) { printf("保存文件\n"); }
void cmd_quit(void) { printf("退出程序\n"); }
int main(void) {
command_fn menu[] = {cmd_new, cmd_open, cmd_save, cmd_quit};
int choice;
printf("0-新建 1-打开 2-保存 3-退出\n请输入选择: ");
scanf("%d", &choice);
if (choice >= 0 && choice <= 3) {
menu[choice](); // 通过下标调用对应的函数
} else {
printf("无效选择\n");
}
return 0;
}
这种“函数表”的设计,让添加新命令变得极为简单——只需写新函数,再把它加入数组即可,不用改任何 if-else 或 switch 逻辑。
五、常见错误与陷阱
1. 函数指针类型不匹配
int add(int a, int b) { return a + b; }
double (*fp)(double, double) = add; // 错误!返回类型和参数类型都不匹配
函数指针赋值时,类型必须完全一致。如果确实需要转换,必须显式强制转换(且后果自负)。
2. 忘记写括号
int *fp(int, int); // 这是函数声明,返回 int*,不是函数指针!
int (*fp)(int, int); // 这才是函数指针
括号是函数指针声明的灵魂,丢了它语义就彻底变了。
3. 对函数指针使用 sizeof
sizeof(add); // 非法!不能对函数名使用 sizeof
sizeof 只能用于对象类型,不能用于函数。
4. 解引用函数指针时的困惑
int (*fp)(int, int) = add;
fp(3, 4); // ✅ 可以直接调用
(*fp)(3, 4); // ✅ 也可以解引用后调用
(**fp)(3, 4); // ✅ 甚至这样也行!
因为函数名本身就是地址,解引用后还是函数类型,又会自动转回地址。所以怎么写都能跑。不用纠结形式,直接用指针名加括号调用最简洁。
六、小结与下篇预告
今天我们把函数和指针结合了起来。核心收获:
- 函数在内存中有地址,函数名就是地址。
- 函数指针声明格式:
返回类型 (*指针名)(参数类型列表); - 用
typedef简化函数指针类型,告别复杂声明。 - 函数指针作为参数,实现回调,是策略模式和解耦的利器。
- 函数指针数组可以用来构建简单的函数表,避免冗长的条件分支。
理解函数指针,你就推开了通往高阶 C 编程的一扇大门。标准库的 qsort、bsearch、signal 都依赖它,大型项目中的插件架构、中断向量表也离不开它。
到目前为止,我们所有的数据——数组也好、字符串也罢——都是在编译时就定好大小的。但如果程序运行时才知道需要多少内存呢?比如用户的输入长度不确定,或者要根据数据库查询结果动态创建数据结构。这就是下一篇的主题:动态内存分配。malloc、free、calloc、realloc,以及堆与栈的根本区别。那是 C 语言又一块充满力量与危险的领域,准备好你的指针知识,我们将长驱直入。
课后小练习
- 写一个函数
apply(int arr[], int n, int (*fn)(int)),对数组的每个元素调用fn,将返回值写回原位置。测试:写一个square函数,将数组元素变成它的平方。 - 用函数指针数组实现一个简单的加减乘除计算器,用户输入两个数和运算符(+、-、*、/),程序通过查表调用对应函数输出结果。
- 解释以下声明的含义:
提示:用右左法则,或从变量名向外读。int (*p1)(int, int); int *p2(int, int); int (*p3[4])(int, int); int (**p4)(int, int); - (小挑战)自己实现一个简化版的
qsort,名字叫my_qsort,支持对int数组排序,接收一个比较函数指针作为参数。用你自己的排序算法(冒泡或选择排序),体验回调函数的实际运用。
我们下期见!
350

被折叠的 条评论
为什么被折叠?



