2. 函数参数:形式参数与实际参数,传值调用与传址调用

好,咱们今天聊点实在的。函数参数这块,说白了就是“怎么把东西递给函数”。很多新手写代码,函数调着调着就崩了,十有八九是参数没搞明白。我当年带项目时,有个同事为了省事,全局变量满天飞,结果查一个bug查了三天——最后发现是参数传错了。嗯,从那以后,我对参数传递这件事就特别较真。

2.1 形式参数 vs 实际参数:别搞混了

先看个最简单的例子:

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    printf("swap内部: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
swap内部:a=20, b=10
调用后:x=10, y=20

看到了吗?swap内部确实交换了,但main里的x和y纹丝不动。这就是形式参数和实际参数的区别。

  • 形式参数(形参):函数定义时写在括号里的变量,比如 int a, int b。它们只是“占位符”,在函数被调用时才分配内存。
  • 实际参数(实参):调用函数时传进去的具体值,比如 xy。它们是实实在在的数据。

我个人习惯把形参想象成“函数门口的收件箱”,实参就是“你投进去的信”。信的内容被复制了一份放进收件箱,原件还在你手里。你想想看,收件箱里的信被烧了,跟你手里的原件有什么关系?

核心要点:形参是实参的副本。函数内部修改形参,不会影响实参。

2.2 传值调用:最常用的方式,但有个坑

传值调用,就是上面那种方式——把实参的复制一份给形参。这是C语言默认的参数传递方式。好处是安全,函数内部随便折腾,不会污染外部变量。坏处嘛……你刚才也看到了,想通过函数修改外部变量?没门。

我在项目中遇到过一个问题:有个同事写了一个“初始化结构体”的函数,传进去一个结构体变量,在函数里给各个成员赋值。结果返回后,结构体还是空的。他百思不得其解,跑来找我。我一看,传值调用嘛,函数里改的是副本,原件当然没变。他当时那个表情……嗯,我至今记得。

小提示:如果函数参数是一个很大的结构体,传值调用会把整个结构体复制一遍,既费时间又费内存。这时候就要考虑传指针了。

2.3 传址调用:想改外部变量?用指针

传址调用,说白了就是传递变量的地址。形参是指针变量,它指向实参的内存位置。这样函数内部通过指针就能直接修改实参的值。

还是那个交换的例子,用传址调用改写:

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
    printf("swap内部:*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
swap内部:*a=20, *b=10
调用后:x=20, y=10

这次真的交换了。为什么?因为 a 指向 x 的地址,b 指向 y 的地址。通过 *a*b 操作,直接修改了 xy 本身。

警告:传址调用虽然强大,但容易出错。我曾经见过有人传了空指针进去,函数里直接解引用,程序瞬间崩溃。所以用指针前一定要检查是否为NULL。

2.4 传值 vs 传址:一张表说清楚

对比项 传值调用 传址调用
传递内容 变量的值 变量的地址
形参类型 普通变量 指针变量
能否修改实参 不能
内存开销 复制数据,大结构体开销大 只复制地址(4/8字节),开销小
安全性 高,不会意外修改外部数据 低,容易误操作或访问非法地址
典型用途 计算、判断、不需要修改外部数据 修改外部数据、操作数组/结构体、动态内存分配

我个人建议:能用传值就别用传址。除非你真的需要修改外部变量,或者参数是个大块头(比如结构体、数组),否则传值更安全、更清晰。你想想看,指针满天飞的代码,调试起来多痛苦。

2.5 避坑指南:我曾经踩过的雷

  • 混淆传值和传址:写函数时,如果参数是指针,记得在函数内部用 * 解引用。我见过有人写了 void func(int *p),然后在函数里直接 p = &new_value;——这改的是指针本身,不是指针指向的值。正确的做法是 *p = new_value;
  • 忘记取地址:调用传址函数时,实参前面要加 &。比如 swap(&x, &y);,写成 swap(x, y); 编译器会报错,但如果你不小心把形参类型写错了,可能编译通过但运行结果不对。
  • 数组名本身就是地址:数组作为函数参数时,传的是首元素的地址,不是整个数组的副本。所以 void func(int arr[])void func(int *arr) 是完全等价的。在函数里修改 arr[i],会直接修改原数组。

记住一句话:传值调用是“我给你一份复印件”,传址调用是“我告诉你我家的保险柜密码”。复印件你随便涂,原件没事;但密码给了你,里面的东西就危险了。

2.6 知识体系:一张图看懂参数传递

下面这张图总结了本章的核心逻辑,我建议你多看几遍,把流程刻在脑子里。

函数参数传递:传值调用 vs 传址调用 传值调用 传址调用 形参 = 实参的副本 修改形参不影响实参 适用:计算、判断、小数据 形参 = 实参的地址 通过指针修改实参 适用:修改外部变量、大结构体、数组 选择哪种方式?看需求!

这张图把两种调用方式的路径和适用场景都画出来了。你可以看到,传值调用是“复制数据”,传址调用是“共享地址”。选择哪种,取决于你到底想不想让函数修改外部变量。

我的习惯:写函数时,先问自己三个问题——1. 这个函数需要修改外部数据吗?2. 参数占内存大吗?3. 调用方是否希望数据安全?回答完这三个问题,选哪种调用方式就一目了然了。


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