6. 指针与函数:打通C语言的任督二脉
指针和函数,这两个东西单独拎出来都不算难。但把它们放在一起,才是C语言真正发力的地方。我做了这么多年嵌入式开发,可以说90%的复杂问题,最后都归结到指针与函数的配合上。今天咱们就把这块彻底讲透。
6.1 指针作为函数参数:传址调用
先问个问题:为什么C语言函数默认是“值传递”?说白了,就是函数拿到的是实参的副本。你改副本,原件不受影响。这在大多数时候是好事——安全。但有些场景,你必须改原件。
核心区别:
- 传值调用:函数内修改形参,不影响实参
- 传址调用:函数通过指针修改实参指向的内存
看个经典例子——交换两个变量的值:
// 错误示范:传值调用
void swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
// 这里改的是副本,外面纹丝不动
}
// 正确做法:传址调用
void swap_right(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swap_wrong(x, y); // x=10, y=20 没变
swap_right(&x, &y); // x=20, y=10 成功交换
return 0;
}
我在项目中遇到过好几次新手犯这个错。有个同事调试了两天,最后发现是传值调用,数据根本没传回来。嗯,从那以后我写函数参数时,只要涉及“修改外部数据”,一律用指针。
我的习惯:函数参数中,如果是指向输入数据的指针,加 const 修饰;如果是输出参数(要被修改的),不加 const。这样读代码时一目了然。
6.2 指针作为函数返回值
函数返回指针,这事很常见。比如动态分配内存、查找数据、返回字符串等。但这里有个大坑——绝对不能返回局部变量的地址。
// 危险!返回局部变量的地址
int* get_local() {
int local_var = 42;
return &local_var; // 函数结束,local_var 被销毁
}
// 正确做法:返回静态变量或堆内存
int* get_static() {
static int static_var = 42;
return &static_var; // 静态区,函数结束后依然存在
}
int* get_heap() {
int *p = (int*)malloc(sizeof(int));
if (p) *p = 42;
return p; // 堆内存,记得在外面 free
}
我曾经在一个通信协议栈里犯过这个错。返回了一个局部数组的指针,结果数据在函数返回后被覆盖,整个协议解析全乱了。调试了整整一个下午,最后用 printf 逐行打印才发现——局部变量已经变成垃圾值了。
避坑指南:
- 永远不要返回局部变量的地址
- 返回堆内存时,调用者必须负责
free - 返回静态变量时,注意线程安全问题
6.3 函数指针:把函数当数据用
函数指针,说白了就是“指向函数的指针”。你想想看,变量有地址,函数也有地址啊。函数指针让我们可以把函数作为参数传递、作为返回值返回、甚至放在数组里。
定义方式有点绕,但记住一个口诀:先写返回类型,再写指针名,最后写参数列表。
// 定义一个函数指针类型
// 指向:返回 int,参数为两个 int 的函数
int (*func_ptr)(int, int);
// 用 typedef 简化
typedef int (*Operation)(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 main() {
Operation op = add;
printf("%d\n", op(10, 5)); // 15
op = sub;
printf("%d\n", op(10, 5)); // 5
// 函数指针数组
Operation ops[] = {add, sub, mul};
for (int i = 0; i < 3; i++) {
printf("%d\n", ops[i](10, 5));
}
return 0;
}
我在嵌入式项目里常用函数指针做“状态机”和“回调函数”。比如按键处理:按一下执行A功能,长按执行B功能。用函数指针数组,代码干净又灵活。
函数指针的典型应用场景:
- 回调函数:比如
qsort的比较函数 - 状态机:每个状态对应一个处理函数
- 插件架构:动态加载不同的算法
- 中断处理:注册不同的中断服务函数
6.4 知识体系总览
下面这张图,把指针与函数的关系梳理清楚了。我建议你保存下来,写代码时对照着看。
6.5 实战中的避坑经验
最后分享几个我踩过的坑,希望能帮你省点时间:
- 函数指针类型不匹配:我曾经把一个
int (*)(int)赋值给int (*)(int, int),编译没报错,运行时栈乱了。嗯,从此我养成了用typedef的习惯。 - 返回局部指针后使用:前面说过了,这是新手最容易犯的错。我建议你在函数注释里明确标注“返回堆内存,调用者需free”。
- 函数指针数组越界:用函数指针数组做状态机时,一定要检查状态值是否在合法范围内。我曾经因为状态值溢出,调用了未知地址的函数,系统直接崩溃。
我的调试技巧:遇到指针相关的问题,先检查三件事——指针是否初始化、指针是否越界、指针指向的内存是否已被释放。80%的问题都能在这三步里找到答案。
指针与函数的配合,是C语言从“会用”到“精通”的分水岭。你想想看,掌握了传址调用、返回指针、函数指针这三板斧,基本上就能驾驭大部分C语言项目了。多写、多练、多踩坑,慢慢就熟练了。