1. 为什么“形参、实参”这个基础概念最容易被疏忽
1.1 从一次“莫名其妙的数组被改动”说起
先讲一个让我印象挺深的排查经历。当时在做一个支付相关的回调模块,C++写的,回调函数里需要把上游返回的一串数据解析后塞进一个全局的结构体数组里。代码写完后一跑,发现第一次回调数据正常,第二次回调一来,前面那一批数据里的一部分字段就变成了乱码。我当时的第一反应是“谁动了我的全局数组”,于是满世界找野指针、找越界,折腾了大半天,最后在同事的提醒下才发现问题出在一个毫不起眼的函数签名上——我当时把一个指针类型的形参直接赋值给了另一个局部指针,然后通过这个局部指针去改数据,看似改的是原数组,实际上因为形参被重新赋值了,地址早就偏了。
这件事让我意识到,形参和实参这个基础概念,平时觉得不值一提,可一旦碰到指针、数组、引用混在一起的时候,几乎所有人都会翻车。尤其对于刚接触C/C++的同学,函数调用时你传进去的那个变量,和你函数内部拿到的那个“东西”,到底是不是同一个,这个问题不管看多少遍书,都不如在调试器里亲眼看一眼栈帧来得清楚。
1.2 形参叫“形式”,实参叫“实际”,名字本身就是提示
很多人学这两个词的时候就是背定义:形参是函数定义时写的参数,实参是调用时传过去的参数。但如果你细品中文翻译,“形式参数”和“实际参数”这六个字其实已经把最关键的信息说透了:形参只是一个“形式上的占位符”,它代表的是“将来这里会有一个值”,但它本身和外面那个变量之间没有任何绑定关系;实参才是“实际参与这次调用的值”。
我习惯把形参比作餐厅门口的叫号牌。你在门口登记的是“张先生”,叫号牌上写“张先生,3位”,但等真正落座吃饭的是张先生这个人,而不是叫号牌上那几个字。函数调用也一样:实参就是“张先生本人”,形参就是“叫号牌”,调用开始时系统把“张先生”的名字抄到叫号牌上,之后就全是叫号牌的事了,你本人怎么变化,叫号牌上是不会同步更新的——除非你把自己的地址写在叫号牌上,服务生才能顺着地址找到你本人。
这个类比可以解释后面所有的坑。值传递、指针传递、引用传递,本质上都是“抄到叫号牌上”的方式不同:值传递抄的是你本人的名字,指针传递抄的是你家的门牌号,引用传递干脆把叫号牌撕下来贴在你脑门上。三种方式对函数内部“能不能找到你本人”的影响完全不同,这就是C/C++参数机制的全部秘密。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 值传递底层到底发生了什么:副本、栈帧与地址
2.1 函数调用时栈上发生了什么
很多人知道“值传递是拷贝”,但不知道拷贝到底发生在哪、发生在什么时候。我建议每个学C/C++的人都亲手做一次这件事:在函数外面定义一个变量 int a = 100;,调用函数 void func(int x),在函数里打印 &a 和 &x。你会发现两个地址完全不同,哪怕 x 的值确实等于100。这一步看懂的人,基本上就理解了值传递的七成。
从计算机底层的角度看,函数调用时操作系统会为每个调用的函数分配一段栈空间,这段空间叫栈帧。形参变量就住在被调函数的栈帧里。调用发生时,实参的值被复制到形参的内存单元中,从这一刻开始,函数体内的所有操作都只针对栈帧里那个形参副本,和外部实参再无关系。函数返回时,栈帧被收回,形参也就随之消亡。
这也是为什么下面这段代码跑完,a 的值依然是10:
c复制void add(int x) {
x = x + 5;
}
int main() {
int a = 10;
add(a);
// 这里 a 仍然是 10
return 0;
}
你可能会说“这么简单的我知道啊”。别急,接下来的问题才是重点:如果把 int 换成一个结构体,再把结构体传进去,里面的数组、字符串、嵌套结构体会不会排除在拷贝之外?答案是不会。C/C++的值传递是逐字节整体复制,结构体里哪怕有10万个字节,也会全部复制到函数栈里。这也是为什么大结构体传值会导致性能骤降——你以为传的是个“指针”,实际上函数入口处就默默地发生了一次完整的深拷贝。
2.2 传指针“改不了”原变量的三种典型场景
很多人学完指针传递后形成了条件反射:只要想改函数外的变量,就传指针,传了指针就能改。这句话在80%的情况下是成立的,但剩下的20%恰恰是全世界的坑。我整理过自己带的学生和同事丢过的三类典型场景。
场景一:对指针形参重新赋值
c复制void swap(int* p1, int* p2) {
int* tmp = p1;
p1 = p2;
p2 = tmp;
// 这里只交换了形参指针本身,外面实参指针没有变化
}
这个例子比较直白,但放到链表中就容易暴露:你写了一个 Node* p = findNode(head, 5);,想通过函数把 p 挪到下一个节点,于是写了 void moveNext(Node* current) { current = current->next; },调用
