在软件设计师与系统架构设计师的历年考题中,函数参数传递机制是每年稳定出现的高频考点,而传值调用与传引用调用的区分,恰恰是命题人最偏爱设置陷阱的位置。许多考生把C语言里的指针当成传引用,把Java里的对象引用误认为传值,结果在四选一的计算题上反复丢分。这篇文章从参数传递的底层内存模型讲起,逐层拆解传值、传引用、传值结果、传名四种机制,再结合软设与架构的历年真题,把命题人的挖坑套路一次讲透,读完这一篇,参数传递相关的选择题和计算题可以做到一算就对。
参数传递讨论的核心问题只有一个:当主调函数把实参交给被调函数时,形参究竟拿到的是实参的一份副本,还是实参本身所在的内存位置。对这个问题的不同回答,划分出了传值调用与传引用调用两大阵营,也决定了函数执行过程中对形参的修改是否会反馈到主调函数的实参上。
传值调用,英文写作 call by value,指的是在函数调用发生时,系统把实参的值复制一份,赋给对应的形参,此后形参在函数体内部的一切读写操作,都作用在这份副本之上,与主调函数中的原变量没有任何关联。形式化地说,传值调用建立的是实参到形参的单向值传递关系,形参是实参的一个全新存储单元,二者在内存中占据不同的地址,函数返回后形参所占用的存储空间随之释放,函数体内对形参的修改不会对实参产生任何影响。C语言、Java语言、Python语言对基本类型参数采用的默认策略都是传值调用,这是理解后续所有概念的第一个锚点。
传引用调用,英文写作 call by reference,指的是函数调用时不复制实参的值,而是把实参的地址(或者说实参的引用)传递给形参,使形参成为实参的一个别名。所谓别名,意味着形参与实参指向同一块内存单元,函数体内部对形参的任何读写操作,实质上都直接作用在实参所在的那块内存上,因此函数返回后,对形参的修改会完整地保留在实参中。传引用调用建立的是实参到形参的双向关联关系,形参不再拥有独立的存储空间,而是借用了实参的存储空间。C++中的引用参数、C语言通过指针显式传递地址、以及若干脚本语言对复合对象的处理,都可以实现传引用调用的效果。
在软考官方教材和历年考试大纲中,参数传递通常归入程序设计语言基础或程序设计语言翻译原理的范畴,与形式参数、实际参数、作用域、存储分配等概念并列出现。教材强调三个关键术语:实参是主调函数中实际存在的变量或表达式,形参是函数定义中声明的占位变量,而参数传递方式决定了两者之间建立的是复制关系还是别名关系。考生在作答时务必区分"传值"与"传引用"这两个术语的英文对应关系,因为选择题的题干经常直接给出 call by value 或 call by reference 的英文表述,避免因术语陌生而误判。
理解参数传递,还需要回到程序运行时的存储分配模型。函数在被调用时,运行系统会为它建立一块独立的活动记录,也叫栈帧,形参就分配在这块栈帧里。传值与传引用的本质区别,恰恰体现在形参这块存储空间里装的是什么:传值装的是实参值的副本,传引用装的则是实参的地址。由此引出两个关键边界。其一,实参的作用域属于主调函数,形参的作用域属于被调函数,两者分处不同的栈帧,即使同名也不构成同一个变量,这是作用域规则与参数传递规则叠加后的基本结论。其二,形参的生存期与函数调用同步,函数一返回,栈帧即被弹出,形参随之消亡,因此在函数内通过取形参地址并保存到外部,会制造出悬空指针或悬空引用,这是把传引用与存储期概念结合命题的常见切入口。考生若能同时看清参数传的是什么、形参活多久、改的是哪个栈帧里的变量这三个问题,参数传递相关的辨析题便能立于不败之地。
要真正理解传值与传引用的区别,不能停留在"传值不改变实参、传引用改变实参"这句口诀上,而必须回到函数调用的底层执行过程,看清实参的值是如何被压入活动记录,形参又是如何在栈帧中分配存储空间的。
在传值调用下,函数调用的执行序列可以分解为三个明确的阶段。第一阶段是实参求值,系统先计算出每个实参表达式的值;第二阶段是形参初始化,系统在函数的活动记录中为形参分配一块全新的存储空间,并把实参的值逐位复制进去,这一复制动作本质上是一次内存拷贝;第三阶段是函数体执行,此后函数体内所有对形参的引用都被解析为指向形参自己的那块副本空间,与实参彻底脱离关系。从内存模型的视角看,调用前后主调函数的实参变量地址始终不变,而被调函数内部多出了一份临时副本,函数返回时这份副本随活动记录的出栈而被回收。正因为传值调用只依赖一次复制,它的实现成本是确定的、低开销的,但也因此丢失了对实参的写回能力。
在传引用调用下,系统跳过了"复制"这一步,转而把实参的地址作为参数传递给被调函数。形参不再是一块独立分配的存储空间,而是被编译成一个指向实参地址的间接引用。函数体内对形参的每次读写,都会先通过这个地址间接寻址到实参所在的内存单元,再执行真正的读写操作。这样形参与实参就成了同一块内存的两种叫法,形参是实参的别名。别名机制的直接后果是:函数体内对形参的赋值会立刻、直接地反映到实参上,主调函数在调用结束后能够观察到实参值的改变。传引用的实现依赖间接寻址,每次访问形参都比传值多一层取地址开销,但它换来了跨越函数边界的写回能力,这一能力在需要"一个函数修改多个外部变量"的场景中不可替代。
把传值与传引用放到一次完整的函数调用生命周期中观察,机制会更加清晰。调用发生时,运行系统依次完成四件事:为被调函数分配活动记录、对实参求值、把实参传递给形参、跳转到函数体执行。传值调用在第三步执行的是复制,把实参的值拷贝进形参单元;传引用调用在第三步执行的是绑定,把形参单元标记为指向实参地址。第四步函数体执行期间,对形参的每次访问都会触发不同的底层动作,传值直接读副本单元,传引用则先取地址再间接寻址。函数返回时,系统做最后两件事:把返回值传回主调函数、回收活动记录。传值结果的写回就发生在传回返回值与回收活动记录之间的狭小窗口里,若函数此时因异常中断而跳过写回步骤,实参将保持调用前的旧值,这也是传值结果与传引用在可靠性上的一个细微却重要的差异。把这条生命周期主线刻在脑子里,无论题目如何变形,都能顺着调用、执行、返回三个节点还原出正确答案。
除了最常考的传值与传引用,软考命题范围还涉及传值结果调用与传名调用两种相对冷门的机制,以及各主流编程语言在参数传递策略上的真实选择。把这些变体梳理清楚,才能在考场上应对题干里出现的各种术语。
传值结果调用,英文写作 call by value-result,可以理解为传值调用与传引用调用的混合体。它在函数调用开始时像传值一样把实参的值复制给形参,在函数体内对形参的操作作用在副本上,但在函数返回时,又额外增加一步:把形参最终的值复制回实参。也就是说,传值结果在函数执行期间对实参无影响,函数返回后才把结果写回,这与传引用在函数执行期间就实时写回形成鲜明对照。传名调用,英文写作 call by name,则是更古老的一种机制,形参在函数体内每被使用一次,就以实参表达式原样替换一次,属于文本替换式的延迟求值,考试中通常只要求知道其名称与大致思想,不涉及深度计算。
理解真实编程语言如何选择参数传递策略,是区分"考试概念"与"工程实践"的关键。C语言对所有参数一律采用传值调用,想要修改外部变量,必须显式传递该变量的地址,即把指针作为参数传入,此时指针本身是按值传递的,但指针所指向的对象可以被修改,这是很多考生产生混淆的根源。C++在传值之外提供了真正的引用参数,用取地址符声明形参即可实现传引用。Java语言对基本类型采用传值,对对象则采用"引用传递值"的方式,即把对象引用的副本传给形参,形参可以通过这个引用修改对象的成员,但无法让实参这个引用本身指向新的对象,因此Java本质上依然是传值。Python与此类似,对象引用按值传递。认清这些语言的真实行为,有助于在架构师的综合题中避开"Java是传引用"的经典陷阱。
从软件工程视角看,参数传递方式的选择是一场可读性、效率与安全性之间的权衡
本篇完!