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 实战中的避坑经验

最后分享几个我踩过的坑,希望能帮你省点时间:

  1. 函数指针类型不匹配:我曾经把一个 int (*)(int) 赋值给 int (*)(int, int),编译没报错,运行时栈乱了。嗯,从此我养成了用 typedef 的习惯。
  2. 返回局部指针后使用:前面说过了,这是新手最容易犯的错。我建议你在函数注释里明确标注“返回堆内存,调用者需free”。
  3. 函数指针数组越界:用函数指针数组做状态机时,一定要检查状态值是否在合法范围内。我曾经因为状态值溢出,调用了未知地址的函数,系统直接崩溃。

我的调试技巧:遇到指针相关的问题,先检查三件事——指针是否初始化、指针是否越界、指针指向的内存是否已被释放。80%的问题都能在这三步里找到答案。

指针与函数的配合,是C语言从“会用”到“精通”的分水岭。你想想看,掌握了传址调用、返回指针、函数指针这三板斧,基本上就能驾驭大部分C语言项目了。多写、多练、多踩坑,慢慢就熟练了。