一、函数调用背后的秘密:传值与传址

函数调用这事儿,说白了就是「把数据交给函数去处理」。但怎么交?交完会不会变?这里头门道不少。我刚开始学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的内存空间。

我曾经踩过的坑:传址调用时,一定要检查指针是否为NULL。有一次我写链表操作,忘了判空,结果传了个空指针进去,程序直接崩溃。从那以后,我写任何指针参数都会先加一句:if (ptr == NULL) return;

二、数组作为函数参数:传的其实是地址

很多初学者会问:「数组作为参数,是传值还是传址?」答案是:传址。数组名本身就是数组首元素的地址。你想想看,如果传值,整个数组复制一份,那效率得多低?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,改的就是原数组。

重要提醒:数组作为参数时,sizeof(arr)在函数内部得到的是指针大小(通常是8字节),不是数组大小。所以一定要额外传一个size参数。我见过太多人在这里翻车了。

三、函数的嵌套调用:一层套一层

嵌套调用,就是一个函数里调用另一个函数。这很常见,比如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层
  • 如果逻辑复杂,拆成多行写
  • 团队协作时,优先考虑可读性

五、知识体系总览

下面这张图,把本章的核心知识点串起来了。我画图时习惯把「传值与传址」作为核心分叉点,因为这是理解函数调用的关键。

函数进阶知识体系 函数调用方式 传值调用 传址调用 复制副本 不改变原值 传递地址 可修改原值 数组作为函数参数 本质是传址 需额外传size

六、总结与避坑指南

好了,这一章的内容就这些。我帮你理一下重点:

特性 传值调用 传址调用
传递内容 变量的值(副本) 变量的地址(指针)
能否修改原变量 不能
内存开销 小(只复制值) 小(只复制地址)
典型应用 计算、判断等只读操作 交换、修改、链表操作等
数组参数 不支持(数组名即地址) 默认传址
我曾经犯过的错:
  • 在函数里用sizeof(arr)算数组长度——得到的是指针大小,不是数组大小
  • 传指针时没判空,导致段错误
  • 嵌套调用太深,栈溢出
  • 链式访问写得太长,自己都看不懂
我的建议:
  • 能用传值就别用传址,安全第一
  • 数组参数一定要带size参数
  • 嵌套调用不超过5层
  • 链式访问不超过3层,超过就拆开
  • 指针参数记得判空

函数调用这块,说白了就是「怎么把数据交给函数,函数怎么还回来」。理解了传值和传址的区别,后面学指针、学数据结构就轻松多了。嗯,这一章就到这儿,下一章我们聊聊更深入的内容。


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