很多刚学 C 语言的人都会遇到一个让人抓狂的问题:
我在函数里明明改了参数,为什么回到
main里一点变化都没有?
更离谱的是,有时候改了又能生效。于是你开始怀疑人生,甚至觉得 C 语言“看心情运行”。
其实这不是玄学,而是 C 语言最基础、也最容易被误解的一个机制:函数调用时,永远只有“传值” 。
这篇文章把这件事彻底讲清楚,让你以后再也不会在参数修改上踩坑。
一、先说结论:C 语言只有“传值”
这是一个非常重要的前提,请先记住它:
C 语言的函数参数传递,永远都是传值(pass by value)。
所谓“传址”,并不是 C 语言有另一种传参方式,而是你传了一个“地址的值” 。
换句话说:
- 传普通变量:传的是变量的值
- 传指针:传的是指针的值(也就是地址)
本质都是传值,只是传的东西不同。
二、最经典的坑:为什么函数里改不动外面的变量
来看一个最常见、也最让人困惑的例子。
1. 普通变量传参
#include <stdio.h>
void change(int x) {
x = 100;
}
int main() {
int a = 10;
change(a);
printf("a = %d\n", a); // 输出 a = 10
return 0;
}
你期望 a 变成 100,但结果还是 10。
为什么会这样?
因为 change(a) 时,发生的是:
x = a; // 把 a 的值拷贝给 x
也就是说:
x是a的一份拷贝- 函数里改的是
x a和x是两个完全独立的变量
函数结束后,x 被销毁,a 当然不会有任何变化。
一个生活化的比喻
你把我书桌上的一本书拍了张照片,拿回去在照片上把书名涂掉。
请问:我书桌上的那本书,书名会变吗?
当然不会。
三、那为什么传数组好像“能改”?
这是第二个让人迷惑的地方。
数组传参的例子
#include <stdio.h>
void modify(int arr[]) {
arr[0] = 99;
}
int main() {
int a[3] = {1, 2, 3};
modify(a);
printf("a[0] = %d\n", a[0]); // 输出 a[0] = 99
return 0;
}
咦?函数里改了,外面也变了。
于是很多人得出结论:
数组是传址的。
错。
真相:数组参数会退化成指针
在 C 语言里,下面两种写法是等价的:
void modify(int arr[]);
void modify(int *arr);
当你写 modify(a) 时,实际发生的是:
arr = &a[0]; // 把数组首元素的地址传进去
你传的依然是“值”,只不过这个值是一个地址。
函数里通过地址去访问内存,当然能改到原来的数据。
四、真正“能改外面变量”的方式:传指针
现在你明白了,要让函数修改外面的变量,必须让它知道变量在哪。
也就是:传地址。
用指针修改普通变量
#include <stdio.h>
void change(int *p) {
*p = 100;
}
int main() {
int a = 10;
change(&a);
printf("a = %d\n", a); // 输出 a = 100
return 0;
}
这次发生了什么?
p = &a; // 把 a 的地址拷贝给 p
注意:
p是&a的拷贝- 但
p里保存的是a的地址 *p = 100意思是:去a的地址上写 100
所以 a 被改到了。
再强调一次本质
你并没有把
a传给函数,而是把a的地址传给了函数。
五、为什么指针本身也可能“改不动”
这是第三个大坑,很多人以为自己已经懂了指针,结果在这里翻车。
想在函数里给指针分配内存
#include <stdio.h>
#include <stdlib.h>
void alloc(int *p) {
p = malloc(sizeof(int));
*p = 10;
}
int main() {
int *q = NULL;
alloc(q);
// q 仍然是 NULL
return 0;
}
你以为 q 会得到一块新内存,结果它还是 NULL。
原因还是一样:传值
调用 alloc(q) 时,发生的是:
p = q; // p 是 q 的一份拷贝,值都是 NULL
函数里改的是 p,不是 q。
函数结束后,p 被销毁,q 当然还是 NULL。
正确写法:传指针的地址
void alloc(int **p) {
*p = malloc(sizeof(int));
**p = 10;
}
int main() {
int *q = NULL;
alloc(&q); // 现在 q 指向新分配的内存
free(q);
return 0;
}
规则没变:
想改谁,就传谁的地址。
六、一张表彻底理清所有情况
| 你想做什么 | 函数参数 | 调用方式 | 原因 |
|---|---|---|---|
| 改普通变量 | int *p | f(&a) | 传变量地址 |
| 改指针变量 | int **p | f(&p) | 传指针的地址 |
| 改数组内容 | int arr[] 或 int *arr | f(a) | 数组退化为指针 |
| 只读取参数 | int x | f(a) | 拷贝值即可 |
七、为什么 C 语言要这样设计?
你可能会问:
为什么 C 语言不直接支持“传引用”?这样不就简单了吗?
这其实和 C 语言的设计哲学有关。
C 语言追求的是:
- 简单
- 高效
- 可预测
- 贴近硬件
如果引入“引用”或“自动传址”,编译器需要额外规则来判断什么时候拷贝、什么时候传址。
而 C 语言只认一条规则:
函数参数就是一份拷贝。
这让行为非常明确,也让程序更高效、更容易推理。
八、一个自检清单,避免以后再踩坑
以后写 C 语言函数时,只要记住下面三句话,基本不会再出错:
-
函数里改不动外面的变量?
- 你大概率传了值,而不是地址。
-
想改谁,就传谁的地址。
- 改普通变量 →
&x - 改指针 →
&p
- 改普通变量 →
-
函数参数永远只是拷贝。
- 拷贝的是值
- 指针也不例外,拷贝的是地址
总结
C 语言函数传参这件事,表面上坑很多,实际上只有一条规则:
C 语言只有传值。
所谓“传址”,只是你传了一个“地址的值”。
只要你始终问自己两个问题:
- 我现在传的是什么?
- 函数里改的是拷贝,还是拷贝指向的东西?
参数修改失败的问题,就再也不会出现了。
