6、指针与函数:函数参数传递(值传递与地址传递)、指针作为函数参数、指针作为函数返回值

好,咱们今天聊一个非常核心的话题——指针和函数怎么配合。说实话,很多初学者在这儿就开始犯迷糊了。我当年带项目的时候,有个同事写了个交换两个数的函数,调了半天就是换不过来……嗯,其实就是值传递和地址传递没搞明白。

6.1 值传递 vs 地址传递——本质区别在哪?

先问大家一个问题:C语言里,函数参数到底是咋传的?

答案只有一个:全部都是值传递。

等等,那地址传递是怎么回事?别急,听我慢慢说。

所谓值传递,就是把实参的副本传给函数。函数里改的是副本,原变量纹丝不动。而地址传递,其实传的也是值——只不过这个值是地址值。你通过这个地址,就能找到原变量,从而修改它。

核心区别一句话:

  • 值传递:函数拿到的是复印件,改复印件不影响原件
  • 地址传递:函数拿到的是原件存放的柜子号,通过柜子号可以改原件

看个经典例子——交换两个数:

// 值传递——失败案例
void swap_wrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // 这里改的是副本,外面不受影响
}

// 地址传递——正确做法
void swap_right(int *pa, int *pb) {
    int temp = *pa;
    *pa = *pb;
    *pb = temp;
    // 通过地址直接操作原变量
}

int main() {
    int x = 10, y = 20;
    swap_wrong(x, y);   // x和y没变
    swap_right(&x, &y); // x和y交换了
    return 0;
}

我在项目中遇到过好几次这种问题——新人写了个排序函数,排完发现原数组没变。一查,传的是数组元素的值,不是地址。说白了,就是没搞清楚「复印件」和「原件」的区别。

6.2 指针作为函数参数——什么时候用?

指针作为参数,主要有三个场景:

  1. 需要修改实参的值——就像上面的swap
  2. 传大块数据避免拷贝——比如传一个结构体指针,而不是整个结构体
  3. 实现「输出参数」——函数返回多个结果

咱们重点说说第三种。你想想看,一个函数只能有一个返回值,那如果我想同时返回商和余数呢?

// 用指针参数实现多返回值
void divide(int a, int b, int *quotient, int *remainder) {
    if (b == 0) {
        *quotient = 0;
        *remainder = 0;
        return;
    }
    *quotient = a / b;
    *remainder = a % b;
}

int main() {
    int q, r;
    divide(17, 5, &q, &r);
    // q = 3, r = 2
    return 0;
}

我个人习惯是:如果函数需要修改某个参数,就在参数名前加个 p 前缀,比如 pQuotient,这样一看就知道是指针参数,要小心使用。

小技巧:如果指针参数只用来读取数据,不修改,记得加 const 修饰。比如 void print(const int *p, int n)。这样编译器会帮你检查,防止不小心改了数据。

6.3 指针作为函数返回值——小心悬空指针

函数返回指针,这个用法很常见,但坑也最多。我当年刚工作时就踩过这个坑……

先看一个正确的例子:

// 返回指向静态变量的指针
int* get_counter() {
    static int counter = 0;
    counter++;
    return &counter;
}

// 返回堆内存的指针
int* create_array(int n) {
    int *arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) {
        return NULL;
    }
    return arr;
}

那什么情况会出问题?看这个:

// 危险!返回局部变量的地址
int* bad_function() {
    int local = 100;
    return &local;  // 函数结束,local被销毁
}

int main() {
    int *p = bad_function();
    printf("%d\n", *p); // 未定义行为!可能崩溃
    return 0;
}

为什么会这样?因为 local 是局部变量,存在栈上。函数返回后,栈空间被回收,那个地址就变成「野地址」了。你拿着它去访问,运气好能读到旧值,运气不好直接段错误。

警告:永远不要返回局部变量的地址!

可以返回的指针:

  • 静态变量(static)的地址
  • 堆内存(malloc)的地址
  • 全局变量的地址
  • 传入的指针参数(比如字符串处理函数返回传入的字符串指针)

6.4 函数指针——把函数当数据用

指针不仅能指向变量,还能指向函数。这个用法在回调、状态机、插件系统里特别常见。

// 定义一个函数指针类型
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 calculate(int x, int y, Operation op) {
    return op(x, y);
}

int main() {
    printf("%d\n", calculate(10, 5, add)); // 15
    printf("%d\n", calculate(10, 5, sub)); // 5
    printf("%d\n", calculate(10, 5, mul)); // 50
    return 0;
}

我在做嵌入式项目时,经常用函数指针数组来实现状态机。每个状态对应一个处理函数,状态切换就是换个函数指针调用。代码干净,扩展也方便。

6.5 本章知识结构图

下面这张图帮你理清指针与函数的关系:

指针与函数 函数参数传递 值传递 传副本,不改原变量 地址传递 传地址,可改原变量 指针作为参数 修改实参值 避免大块数据拷贝 实现多返回值

6.6 避坑指南——我踩过的那些坑

最后,分享几个实战中容易翻车的地方:

我曾经犯过的错:

  • 忘记检查NULL指针——函数里直接解引用传入的指针,结果传了个NULL进来,程序直接崩了。现在我的代码里,只要是指针参数,第一件事就是判空。
  • 返回栈上数组的指针——写过 return &arr[0];,arr是局部数组。调用方拿到指针后,数据已经被覆盖了。排查了好久才发现。
  • 函数指针类型不匹配——回调函数签名和函数指针定义不一致,编译能过,运行时栈乱掉。后来我养成了用typedef定义函数指针类型的习惯。

嗯,指针和函数的配合,说白了就是搞清楚「谁拥有数据,谁修改数据」。想明白了,代码就稳了。


公众号:蓝海资料掘金营,微信deep3321