18、指针(四):函数指针、回调函数、指针作为函数参数
指针这东西,说到函数这块,才真正开始展现它的威力。
我个人习惯把指针比作「遥控器」。你拿着遥控器,不用走到电视机跟前,就能换台、调音量。函数指针和指针参数,干的就是类似的事——你不需要把数据搬来搬去,拿着地址就能操作它。
18.1 指针作为函数参数:别想复制,直接动手
先聊最基础的。C语言的函数参数传递,默认是「值传递」。什么意思?你传进去一个变量,函数拿到的是它的副本。你在函数里改来改去,外面的原变量纹丝不动。
void swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
// 外面的人:你谁啊?我变量没变啊
}
int main() {
int x = 3, y = 5;
swap_wrong(x, y);
printf("x=%d, y=%d\n", x, y); // 还是 3 和 5
return 0;
}
嗯,这里要注意:你传的是值,不是变量本身。函数里 a 和 b 是新的局部变量,跟外面的 x、y 没关系。
那怎么才能真的交换?传地址。
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 3, y = 5;
swap(&x, &y);
printf("x=%d, y=%d\n", x, y); // 5 和 3,成了
return 0;
}
我在项目中遇到过好几次这种问题:新手写链表操作,传了头指针进去,函数里改了头指针,出来发现链表丢了。为什么?因为传的是指针的值,不是指针的地址。想改指针本身,得传指针的指针。
核心原则:
- 想改变量的值 → 传变量的地址(指针)
- 想改指针的值 → 传指针的地址(二级指针)
小技巧:写函数时,如果参数只是「读」数据,用 const 修饰指针。比如 void print(const int *p)。这样编译器会帮你检查,防止不小心改了不该改的东西。
18.2 函数指针:把函数当变量用
函数指针,说白了就是「指向函数的指针」。函数本身也有地址,存在代码段里。你可以把这个地址存下来,通过它去调用函数。
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; // 指向 add 函数
printf("%d\n", op(10, 5)); // 15
op = sub;
printf("%d\n", op(10, 5)); // 5
return 0;
}
你想想看,函数指针的声明语法有点绕:int (*op)(int, int)。先写返回值类型,括号里写指针名,再写参数列表。我刚开始学的时候,老把括号位置搞错。写成 int *op(int, int) 就变成了「返回 int 指针的函数」,完全两码事。
我曾经踩过的坑:函数指针的类型必须严格匹配。返回值、参数个数、每个参数的类型,一个不对编译器就报错。别想着「差不多就行」,C语言不跟你商量。
18.3 回调函数:把选择权交给调用者
回调函数,就是用函数指针实现的一种机制。你写一个通用函数,里面留个「钩子」,让调用者决定具体怎么处理数据。
最经典的例子就是 qsort 排序。排序的逻辑是固定的,但「怎么比较两个元素」得由你决定。
int compare_int(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int main() {
int arr[] = {5, 3, 8, 1, 9};
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), compare_int);
// 第四个参数就是回调函数
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
qsort 不知道你要排整数、浮点数还是结构体。它把比较的「具体操作」交给你。这就是回调的精髓:框架提供流程,你提供逻辑。
我在项目中做过一个事件驱动的模块。底层只管收消息、分发消息。上层注册不同的回调函数来处理不同的事件。底层代码一行不改,上层想加什么功能就加什么功能。这种解耦方式,用起来真的很舒服。
18.4 知识结构图
下面这张图帮你理清本章的核心脉络:
18.5 实用建议
讲到这里,我总结几条实战经验:
- 能用指针传参就别用全局变量。全局变量看着省事,但代码一复杂,你根本不知道谁改了它。指针传参,调用关系一目了然。
- 函数指针的 typedef 能省不少事。比如
typedef int (*OpFunc)(int, int);,后面声明变量就写成OpFunc op;,清爽多了。 - 回调函数注意生命周期。注册的回调函数,在调用时它的地址必须仍然有效。别注册了一个局部函数,退出作用域后回调还在调用它——那是未定义行为,程序随时可能崩。
- 写回调时,多传一个 void *context 参数。这样调用者可以带自己的数据进去,不用依赖全局变量。很多库都这么设计,比如 pthread_create 的最后一个参数。
一个小练习:写一个通用的「遍历数组并应用操作」的函数。它接收数组、长度、元素大小,以及一个回调函数。回调函数负责处理单个元素。试试看,你会发现这种模式在很多地方都能用。
指针和函数的结合,说白了就是「把控制权交出去」。你写一个框架,别人往里填逻辑。你写一个函数,调用者决定怎么用。这种灵活性,正是C语言经久不衰的原因之一。
公众号:蓝海资料掘金营,微信 deep3321