一、函数调用背后的秘密:传值与传址
函数调用这事儿,说白了就是「把数据交给函数去处理」。但怎么交?交完会不会变?这里头门道不少。我刚开始学C语言时,就吃过这个亏——函数里改了参数,回到主函数发现原变量纹丝不动。嗯,这就是传值调用的特点。
1.1 传值调用:你给的是一份复印件
传值调用,就是把变量的值复制一份,交给函数。函数拿到的是副本,原件还在主调函数手里。你想想看,你在复印件上涂涂改改,原件会受影响吗?当然不会。
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("函数内部:a=%d, b=%d\n", a, b);
}
int main() {
int x = 10, y = 20;
printf("调用前:x=%d, y=%d\n", x, y);
swap(x, y);
printf("调用后:x=%d, y=%d\n", x, y);
return 0;
}
运行结果:
调用前:x=10, y=20
函数内部:a=20, b=10
调用后:x=10, y=20
看到了吧?函数内部确实交换了,但回到main函数,x和y还是老样子。我在项目中遇到过类似问题——有个同事写了个排序函数,传进去数组元素,排完序发现原数组没变。排查了半天,才发现是传值调用闹的。
1.2 传址调用:你给的是原件存放的位置
传址调用,传的是变量的地址(指针)。函数通过地址直接操作原变量。说白了,你给了函数一把钥匙,它可以直接进房间搬东西。
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
printf("函数内部:*a=%d, *b=%d\n", *a, *b);
}
int main() {
int x = 10, y = 20;
printf("调用前:x=%d, y=%d\n", x, y);
swap(&x, &y);
printf("调用后:x=%d, y=%d\n", x, y);
return 0;
}
运行结果:
调用前:x=10, y=20
函数内部:*a=20, *b=10
调用后:x=20, y=10
这次真的交换了。为什么?因为传的是地址,函数通过指针直接修改了x和y的内存空间。
二、数组作为函数参数:传的其实是地址
很多初学者会问:「数组作为参数,是传值还是传址?」答案是:传址。数组名本身就是数组首元素的地址。你想想看,如果传值,整个数组复制一份,那效率得多低?C语言设计者没那么傻。
#include <stdio.h>
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void modifyArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2;
}
}
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int len = sizeof(numbers) / sizeof(numbers[0]);
printf("原始数组:");
printArray(numbers, len);
modifyArray(numbers, len);
printf("修改后:");
printArray(numbers, len);
return 0;
}
运行结果:
原始数组:1 2 3 4 5
修改后:2 4 6 8 10
注意看,modifyArray函数直接改了原数组。因为arr本质上就是int*指针。所以你在函数里写arr[i] = xxx,改的就是原数组。
三、函数的嵌套调用:一层套一层
嵌套调用,就是一个函数里调用另一个函数。这很常见,比如A函数调用B函数,B函数又调用C函数。说白了,就是分工合作——每个人只干自己那摊事。
#include <stdio.h>
int max(int a, int b) {
return (a > b) ? a : b;
}
int maxOfThree(int a, int b, int c) {
int temp = max(a, b); // 嵌套调用max
return max(temp, c); // 再次嵌套调用
}
int main() {
int result = maxOfThree(10, 25, 18);
printf("三个数中最大的是:%d\n", result);
return 0;
}
运行结果:
三个数中最大的是:25
这个例子很简单。maxOfThree调用了两次max。执行流程是:main → maxOfThree → max → 返回 → max → 返回 → main。一层层进去,再一层层出来。
嵌套调用的深度没有硬性限制,但别太深。我记得有一次调试一个递归程序,嵌套了上千层,栈空间直接爆了。嗯,每个函数调用都会占用栈空间,嵌套太深容易栈溢出。
四、函数的链式访问:返回值直接当参数
链式访问,说白了就是「函数的返回值直接作为另一个函数的参数」。这样写代码很简洁,但可读性可能会下降。我个人的习惯是:链式不超过三层,超过三层就拆开写。
#include <stdio.h>
#include <string.h>
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
// 链式访问:add的返回值直接作为multiply的参数
int result = multiply(add(3, 4), 5);
printf("结果:%d\n", result); // (3+4)*5 = 35
// 等价写法(拆开)
int sum = add(3, 4);
int product = multiply(sum, 5);
printf("拆开写结果:%d\n", product);
return 0;
}
运行结果:
结果:35
拆开写结果:35
链式访问在标准库中很常见。比如字符串操作:strcpy(str, strcat(buf, "hello"))。但说实话,这种写法可读性差,调试也麻烦。我建议你:
- 链式不超过2-3层
- 如果逻辑复杂,拆成多行写
- 团队协作时,优先考虑可读性
五、知识体系总览
下面这张图,把本章的核心知识点串起来了。我画图时习惯把「传值与传址」作为核心分叉点,因为这是理解函数调用的关键。
六、总结与避坑指南
好了,这一章的内容就这些。我帮你理一下重点:
| 特性 | 传值调用 | 传址调用 |
|---|---|---|
| 传递内容 | 变量的值(副本) | 变量的地址(指针) |
| 能否修改原变量 | 不能 | 能 |
| 内存开销 | 小(只复制值) | 小(只复制地址) |
| 典型应用 | 计算、判断等只读操作 | 交换、修改、链表操作等 |
| 数组参数 | 不支持(数组名即地址) | 默认传址 |
- 在函数里用sizeof(arr)算数组长度——得到的是指针大小,不是数组大小
- 传指针时没判空,导致段错误
- 嵌套调用太深,栈溢出
- 链式访问写得太长,自己都看不懂
- 能用传值就别用传址,安全第一
- 数组参数一定要带size参数
- 嵌套调用不超过5层
- 链式访问不超过3层,超过就拆开
- 指针参数记得判空
函数调用这块,说白了就是「怎么把数据交给函数,函数怎么还回来」。理解了传值和传址的区别,后面学指针、学数据结构就轻松多了。嗯,这一章就到这儿,下一章我们聊聊更深入的内容。
公众号:蓝海资料掘金营,微信deep3321