在软考软件设计师的考试大纲中,函数调用与参数传递属于程序设计语言基础部分的核心内容。这一考点在近四年的上午卷中连续出现,2020年下半年、2021年上半年、2023年上半年以代码跟踪题的形式考查,2023年下半年则以概念判断题的形式直接考查。命题频率之高,说明出题人默认考生必须精确掌握参数传递的语义规则。然而大量考生在这一考点上反复丢分,根本原因在于只记住了"传值不变、传引用变"的口诀,却没有真正理解教材中对这两种传递方式的形式化定义,一旦题目把两种方式组合使用,口诀立刻失效。
要精确理解参数传递,必须先区分形参与实参这一对基本概念。形参是函数定义时写在参数列表中的变量,它在函数被调用之前并不占用存储空间,只是对将来传入数据的一个占位符。实参是函数调用时实际传给函数的表达式或变量的值,它承载着真正要参与运算的数据。软考教材中的标准表述是:形参全称形式参数,实参全称实际参数,两者在函数调用发生时通过参数传递机制建立关联。这个关联的具体方式,正是传值与传引用两种机制的分野所在。
传值调用,教材中也称为值调用,其标准定义是:把实参的值复制一份,存入对应的形参存储单元,此后函数体内对形参的一切读写操作,都发生在这份副本之上,与实参所在的存储单元再无任何关联。换言之,传值建立的是"值的复制"关系,形参是实参值的一个快照。函数返回后,无论形参在函数体内被修改了多少次,实参保持调用前的原值不变。这种机制的优点是安全性高,函数内部的操作不会产生对调用者的副作用,缺点在于当实参是大型结构或数组时,复制动作会带来时间和空间上的开销。
传引用调用,教材中也称为引用调用,其标准定义是:不复制实参的值,而是把实参的引用,也就是实参在内存中的地址,传递给形参,使形参成为实参的别名。此后函数体内对形参的每一次读写,实质上都通过地址间接作用在实参的存储单元上。因此,传引用建立的是"存储单元共享"的关系,函数体内修改形参,实质上就是在修改实参。这种机制让函数具备了通过参数向调用者回传结果的能力,也是实现交换、累加、修改外部状态等操作的基础。软考教材中常把传引用与传地址并提,两者的核心思想一致,都是让形参与实参指向同一存储位置,区别在于语言层面的实现手段不同,后文会详细展开。
需要特别指出的是,软考命题人对这两种定义扣得非常细。2023年下半年第21题就要求考生判断四个叙述的正误,其中"在传引用方式下,修改形参实质上改变了实参的值"是正确答案,而"在传值方式下,形参与实参之间互相传值"以及"在传引用方式下,实参可以是任意的变量和表达式"均为错误表述。考生若不能准确复述定义,仅凭模糊印象作答,极易被这类表述上的细微差别迷惑。
值得强调的是,参数传递只发生在函数调用建立参数绑定的瞬间,规定的是一次调用开始时形参如何获得实参的数据,而不是执行过程中两者之间持续存在的通信信道。传值并非"调用期间实时同步",传引用也不是"调用期间保持连接",两者的差别在绑定瞬间就已确定。理解了这一点,就能正确回答"函数执行中途实参被修改,形参是否感知"这类变式问题——传值形参持有的是绑定时的旧副本,永远感知不到后来的变化;传引用形参始终按地址访问实参当前值,实参的任何变化都会立即反映到形参的读写中。
从考纲定位看,参数传递被软考持续考查,是因为它同时检验三个能力层次:概念层能否准确区分传值与传引用的定义,机制层能否理解函数调用的运行时行为,应用层能否在具体语言环境下判断程序输出。三个层次正好对应概念判断与代码跟踪两种题型,只背结论不追机制,代码题就会暴露短板。教材在程序设计语言基础一章中对函数定义、函数声明、函数调用与参数传递有系统论述,标准术语涵盖值调用、引用调用与传地址调用三种基本形式,其中传地址调用常被视作引用调用的低级实现手段。先把三者关系理清,再做真题,思路会清晰得多。
要真正掌握传值与传引用的区别,必须回到内存层面,理解一次函数调用发生时,计算机究竟在做什么。这里涉及的底层概念是栈帧与活动记录。
函数调用是程序运行时的动态行为,每次调用发生时,运行时系统都会在调用栈上为本次调用分配一段连续的内存区域,这段区域称为栈帧,也译为活动记录。栈帧中依次存放本次调用所需的全部运行时信息:实参的值、返回地址、调用者的栈帧基址、局部变量、临时变量等。当函数返回时,整个栈帧被撤销,栈指针回退,栈帧中的内容随之失效。参数传递机制的本质差异,就体现在实参进入栈帧参数区的那一刻。
在传值方式下,进入栈帧参数区的是实参值的完整副本。以C语言为例,调用函数时,运行时系统首先计算实参表达式的值,然后把这个值写入栈帧中为形参预留的存储单元。此后函数体内对形参的赋值,只是改写栈帧参数区中属于自己的那一份副本,这个存储单元与调用者空间中实参的存储单元在物理上完全隔离。所以传值方式的语义可以概括为八个字:复制进栈,互不相干。
在传引用方式下,进入栈帧参数区的是实参存储单元的地址。函数体内每次访问形参,编译器都会生成一条间接寻址的指令,先取出形参单元中保存的地址,再通过这个地址去读写真正的实参。这意味着函数体内看似对形参的普通赋值,实际上是一次"按地址写内存"的操作,写入的目标正是实参所在的位置。所以传引用方式的语义同样可以概括为八个字:传址进栈,按址访问。理解了这层内存机制,就再也不会把两种方式的后果搞混。
除参数绑定方式外,函数调用的运行时行为还涉及参数求值顺序与调用约定两个细节。参数求值顺序规定多个实参表达式按何种次序计算,C语言标准未作规定,由编译器决定,依赖特定求值顺序的程序不可移植。调用约定则规定参数压栈次序与栈帧清理责任等细节。两者软考极少直接考查,但了解参数区在栈帧中的布局,有助于建立完整的函数调用心智模型。
传值的本质是一次完整的值复制,这个复制动作发生在调用建立参数绑定的瞬间,且只发生一次。复制完成后,形参与实参之间不存在任何后续同步机制。这一点可以解释两个常见的考试现象。其一,函数体内对形参的多次修改,在函数返回后对实参毫无影响,因为修改都落在副本上。其二,传值方式下实参可以是任意表达式,因为运行时只需要表达式的值,并不关心这个值来自哪个存储单元,常量、算术表达式、函数调用的返回值都可以作为实参。
值复制的代价同样值得关注。当参数是整型、字符型这类基本类型时,复制一次的成本几乎可以忽略;但当参数是大型结构体、长字符串或大数组时,完整复制的时空开销会显著上升,这也是程序设计语言纷纷引入引用机制的直接动因。
传引用的本质是别名绑定。形参一旦与实参绑定,就与实参共享同一个存储单元,成为实参在函数体内的另一个名字。C++语言中引用类型的设计最能说明这一点:引用必须在定义时完成绑定,之后不能改绑到其他对象,对引用的任何操作都等价于对绑定对象的操作。从这个角度看,传引用的形参不是一个独立的变量,而是实参的别名。
别名绑定带来两个推论。第一个推论是函数可以通过修改形参来修改实参,这正是传引用机制的价值所在,交换函数、累加函数、从函数中带回多个结果的实现都依赖于此。第二个推论是传引用方式对实参有严格的左值要求:实参必须是一个真实存在的存储单元,也就是变量,而不能是常量或复杂表达式,因为别名机制需要一个可以绑定的地址。2023年下半年第21题的D选项"在传引用方式下,实参可以是任意的变量和表达式"之所以错误,原因正在于此。常量没有可绑定的独立地址,表达式的值在运算完成后也未必持久存在于某个可寻址的单元中,它们都无法作为别名的绑定目标。
把传引用与函数返回值对比,能进一步巩固理解。返回值是函数结束时的单向出口,一次调用只能回传一个值;传引用参数则是贯穿函数执行全程的双向通道,既可以传入数据,也可以随时回写结果,一次调用可以借助多个引用参数带回多个结果。真题中常见的"交换两个变量的值"题目就是经典例证:用传值实现的交换函数在返回后两个变量一切照旧,只有引用语义才能完成真正的交换,这个例子是检验考生是否真懂传引用的试金石。
软考教材在传值与传引用之外,还涉及传地址、传对象引用等衍生概念,考生需要把这些概念放在一个统一的框架中理解。
第一类是传值调用。实参值复制给形参,形参修改不影响实参,实参可以是任意表达式,安全性最高,代价是复制开销。第二类是传引用调用。形参绑定为实参的别名,形参修改直接影响实参,实参必须是变量,适合需要回传结果的场景。第三类是传地址调用,以C语言传指针为代表。从语义上看,C语言只有传值一种机制,所谓传指针,传的其实是地址值的副本,函数内通过解引用操作间接修
本篇完!