最近逛技术社区的频率有点高,隔三差五就能看到有人贴出一张报错截图,上面写着“无法将 npm 项识别为 cmdlet、函数、脚本文件或可运行程序的名称”,隔几天又变成“无法将 claude 项识别为……”,再隔几天是“无法将 git 项识别为……”。这些报错本质上都是环境变量没有配置好,命令找不到可执行文件,跟函数本身没有直接关系。但每次看到“函数”这个词,我都会想起另一个让无数人抓狂的场景:代码明明写得很顺手,函数也调用成功了,输出的结果却死活不对。这类问题十有八九和函数的参数传递机制有关,也就是我们今天要聊的“值传递”。这篇文章会用 C、C++、Java、Python、JavaScript 五种语言做对比,把值传递的原理、常见误区、经典 Bug 和工程选型一次讲透。无论你是刚入门的新手,还是写了几年业务代码的老手,我都建议把这件事彻底想明白,因为它决定了你能不能准确预判“函数内部改了参数,外面到底变不变”。
1. 为什么改参数改不动外面的变量:值传递的第一课
1.1 一个让新手集体破防的 swap 函数
不知道你有没有写过这样一段代码:在函数里交换两个变量的值,打印出来一切正常,回到调用处一看,两个变量纹丝不动。我之前在一个交流群里见过一位老哥贴出下面这段 C 语言代码,配文是“天塌了”:
c复制void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swap(x, y);
printf("x=%d, y=%d\n", x, y); // 输出:x=10, y=20
return 0;
}
他盯着屏幕上“x=10, y=20”看了半天,怎么都想不明白:明明函数里面已经交换了啊,怎么一出来就不认账了?
这个问题十个新手有九个会遇到,甚至不少写了两年项目的人,遇到类似情况依然会愣一下。原因很简单:函数参数默认是“值传递”。调用 swap(x, y) 的时候,x 的值 10 和 y 的值 20 会被分别拷贝一份给函数内部的形参 a 和 b,函数内部的交换操作全都发生在 a 和 b 这两个副本身上。函数一结束,副本销毁,x 和 y 自然什么影响都没受到。
1.2 形参和实参:函数边界上的“复印件”
要彻底理解这件事,首先要分清形参和实参。
实参是调用函数时写在括号里的变量或表达式,它是真实存在的数据源;形参是函数定义时写的参数名,本质上是一个局部变量,作用域仅限于函数内部。函数被调用的那一刻,系统会把实参当前的值拷贝一份,塞给形参。从此以后,形参和实参就是两个独立的个体——名字像、值一样,但内存地址完全不同。
你可以这样记:值传递相当于你递给办事窗口一份复印件,工作人员在复印件上随便涂改、盖章,你的原件永远不会被波及。如果你希望工作人员直接在原件上操作,那就必须把“原件放在哪儿”这个信息告诉对方,也就是传地址——这是后面指针和引用要做的事。
1.3 内存视角:拷贝发生在哪里
如果从内存视角看,值传递的整个过程发生在函数调用栈里。调用函数时,系统会为这次调用创建一块新的栈帧,形参就在这块栈帧里分配空间,然后把实参的值复制进去。函数运行期间,它访问的始终是这块新栈帧里的副本;函数返回时,栈帧释放,副本随之消失。这也是为什么“函数内部的一切修改,默认在外部都无影无踪”。
这一段如果之前没认真想过,建议在纸上画一画:x=10 写在左边一格,函数栈里 a=10 写在右边一格,两个格子之间没有线连接,只有一次复制动作。想通了这一点,后面所有关于引用、指针、对象的迷思都会迎刃而解。
1.4 为什么需要拷贝?函数隔离带来的安全感
有人可能会问:既然拷贝这么麻烦,为什么不直接让函数用外面的变量,这样性能更好、代码也更节省?答案在于,函数需要具备“隔离性”。
如果函数可以随意修改外部变量,那么一个函数的行为就不再由它的参数和返回值决定,而会被调用环境里一堆乱七八糟的全局状态牵着鼻子走。你调用一个函数,不知道它会偷偷改掉哪些变量,排查问题的时候只能泪流满面地到处打断点。值传递提供了一个天然的边界:函数里面怎么折腾,都不会波及外界的实参,除非你主动通过指针、引用或者返回值来打破这个边界。这种边界感是代码可维护性的基石,也是为什么短短一个 swap 失败案例,值得被拿出来反复讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 指针也是值:C 语言里最容易被误读的“传引用”
2.1 传地址的本质:传递的是地址值
理解了“值传递”之后,再看 C 语言里的指针传递,很多人的第一反应是:那传指针应该就是传引用了吧?
不是。C 语言根本没有引用这种语法(C++ 才有),传指针本质依然是一次值传递,只是这次拷贝的不是数据本身,而是数据的地址值。
c复制void swap(int *a, int *b) {
int te
