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 指针作为函数参数——什么时候用?
指针作为参数,主要有三个场景:
- 需要修改实参的值——就像上面的swap
- 传大块数据避免拷贝——比如传一个结构体指针,而不是整个结构体
- 实现「输出参数」——函数返回多个结果
咱们重点说说第三种。你想想看,一个函数只能有一个返回值,那如果我想同时返回商和余数呢?
// 用指针参数实现多返回值
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