在程序设计语言中,函数又称过程或子程序,是组织代码的基本单元。函数在被定义时,函数名后面括号内列出的变量称为形式参数,简称形参。形参的本质是一个占位符,它规定了调用者调用这个函数时需要提供的参数个数、参数类型和参数顺序,但形参本身在函数定义阶段并不持有具体的数值。函数在被调用时,调用者传入的具体数值或变量称为实际参数,简称实参。实参是真实存在的数据,它要么是一个具体的字面常量,要么是一个已经在调用环境中分配了存储空间的变量。
形参与实参在内存中的位置截然不同。形参在函数被调用时才被系统分配存储空间,函数调用结束之后,形参所占用的存储空间就会被释放,形参的生命周期与函数执行过程同步。实参则存在于调用者所在的调用环境之中,它的存储空间在调用发生之前就已经分配好,它的生命周期由调用环境决定,与函数是否执行没有必然联系。这一区分是整个参数传递问题的基础,因为参数传递机制要解决的核心问题,就是形参与实参之间数据究竟如何对接:是把实参的值复制一份交给形参,还是让形参直接指向实参所在的存储单元。
从语言规范的角度看,形参的作用域局限于函数体内部,函数体内可以自由读写形参,但函数体之外无法直接访问形参。实参的作用域则属于调用者,它在函数体外部依然有效。当形参名与实参名恰好相同时,二者依然是两个完全独立的存储单元,只是名字碰巧相同而已,这一点在分析嵌套调用时尤其重要。
参数传递机制,又称参数结合方式或参数绑定方式,指的是在函数调用过程中,实参的值如何传递给形参的规则。在软考软件设计师的考纲中,参数传递通常被归纳为两大类:值传递和引用传递。
值传递,又称传值调用,英文表述为 call by value。它的规则是:在函数调用时,系统先计算出实参的值,然后把这个值的副本存入形参对应的存储单元。此后,函数体内部对形参进行的任何读取和修改操作,都是针对这个副本进行的,与实参本身没有任何关系。
引用传递,又称传引用调用,英文表述为 call by reference。它的规则是:在函数调用时,形参并不获得实参值的副本,而是直接获得实参存储单元的访问方式,形参成为实参的别名。函数体内部对形参的修改,会直接反映到实参上。
理解了这两种机制的定义,就能抓住一个贯穿始终的判断标准:值传递保护实参不受函数内部修改的影响,引用传递则允许函数内部修改实参。这一判断标准贯穿了软考历年对参数传递的所有考查,是答题的出发点。
值传递的底层原理是一次完整的数据复制。当函数被调用时,系统会在调用栈上为形参开辟独立的存储空间,然后把实参当前的值逐一复制到这段新开辟的空间之中。复制完成之后,形参和实参就是两个互不相干的存储单元,它们之间唯一的联系,就是复制发生那一刻的数值相等。
可以用栈帧的概念来理解这个过程。每次函数调用都会在内存中创建一个新的栈帧,栈帧中保存着局部变量、形参、返回地址以及一些必要的现场信息。值传递时,实参的值被复制进新栈帧的形参位置。当函数返回时,整个栈帧被弹出销毁,形参的存储空间随之消失。因此,无论函数内部如何修改形参,这些修改都只发生在那个即将被销毁的栈帧之中,实参在调用者的栈帧里安然无恙。
值传递的一个直接后果是复制的开销。如果实参是一个大型的结构体或者对象,值传递意味着要把整个对象复制一遍,这既消耗时间也消耗内存。正是这种复制开销,推动了多种语言引入引用传递或者指针机制。软考命题人有时会从这个角度出题,考查考生对复制开销与传递效率之间关系的理解。
值传递还有一个重要的语义特征,就是实参与形参在调用期间完全隔离,函数内对形参的任何运算都不会波及实参,这带来了程序的可预测性与安全性。
引用传递的底层原理是别名机制。引用传递并不复制实参的值,而是让形参直接绑定到实参的存储单元之上,形参和实参指向内存中的同一块区域。对形参的任何操作,本质上都是对这块内存区域的操作,因此修改形参就等于修改实参。
别名机制的实现方式因语言而异。在 C++ 语言中,引用通过在类型名后加引用符来声明,一个引用在初始化之后就永久绑定到被引用的对象上,使用引用就像使用原变量一样自然。
理解别名机制,关键是要理解"同一块内存,两个名字"这句话。值传递是"两块内存,两个名字,值相同";引用传递是"一块内存,两个名字"。这个对比是回答参数传递判断题的利器。当题目给出一个函数并要求判断实参是否会被修改时,只要判断传递方式是值传递还是引用传递,答案就基本确定了。
引用传递的效率优势也来自别名机制,由于不需要复制整个对象,传递大型结构体或对象时几乎不产生额外开销,这也是 C++ 大量使用常量引用传递只读大对象的原因。
在 C 语言和 C++ 语言中,还存在一种常被考生与引用传递混淆的机制,即地址传递,也常称为指针传递。地址传递的本质是:把实参的地址作为一个值复制给形参,形参是一个指针变量。这里必须强调一个关键事实:地址传递本质上仍然是一种值传递,只不过被复制的那个"值"是一个地址而已。
这一点是软考命题人最爱挖坑的地方。很多考生看到"传地址"就以为是引用传递,实际上,地址传递中,形参指针本身是实参指针的一个副本。如果函数内部修改了形参指针本身,比如让它指向另一个地址,实参指针不会改变。但如果函数内部通过形参指针去解引用、去修改指针所指向的内存内容,那么这块内存的内容确实会被改变,因为形参指针和实参指针指向的是同一个地址。
一句话总结:地址传递复制的是地址值,引用传递绑定的是存储单元。前者修改指针指向的内容会生效,修改指针本身不会生效;后者修改任何东西都会生效。这个辨析是区分两类机制的核心,也是历年真题反复验证的命题点。考生务必把"传指针"与"传引用"在概念上彻底分开,前者属于值传递家族,后者才是真正的引用传递。
C 语言从设计之初就只有值传递这一种参数传递机制。在 C 语言中,无论实参是基本类型、结构体还是指针,函数调用时都是把实参的值复制一份给形参。对于基本类型,复制的是数值;对于结构体,复制的是整个结构体的内容;对于指针,复制的是指针变量中保存的地址值。
正因为 C 语言只有值传递,所以当程序员希望在函数内部修改外部变量的值时,就必须借助指针。具体做法是把变量的地址传给函数,函数内通过解引用操作来间接修改该地址处的内存。这时的"修改"并不是修改形参本身,而是修改形参指针所指向的内存,因此能够反映到外部变量上。考生必须牢记:C 语言中所谓的"传址",其实是在值传递的框架下,用指针间接访问来实现的,它并没有脱离值传递的范畴。
C 语言值传递的纯粹性,是软考命题的重要背景。当题目给出一段 C 语言代码并要求判断变量在调用后的值时,答案往往遵循"值传递保护实参"的原则,除非代码中显式使用了指针解引用。
C++ 语言在 C 语言的基础上引入了引用类型,从而真正拥有了引用传递。C++ 中的引用用引用符声明,一个引用必须在声明时就初始化,并且一旦绑定就不能再改绑其他对象。当函数参数声明为引用类型时,调用就是引用传递,形参直接成为实参的别名。
C++ 中引用传递的典型用途有两个:一是在函数内部修改实参的值,比如交换两个变量的值;二是避免大对象的复制开销。软考通常结合这两个用途出题,让考生判断参数应声明为值类型还是引用类型。
在 C++ 中还存在常量引用,即在引用符前加常量限定,表示函数可经引用访问实参但不允许修改。常量引用既获得引用传递零复制的高效,又获得值传递不修改实参的安全,是大对象只读参数的最佳选择。
Java 语言和 Python 语言的参数传递机制是软考命题的高频陷阱区。这两个语言的官方规范都明确表示只有值传递,但实际编程中,把一个对象传给方法后,方法内对对象属性的修改确实会影响到外部对象,这常让考生困惑。
要理解这个现象,需要区分"对象"和"对象的引用"这两个概念。在 Java 中,变量里保存的并不是对象本身,而是对象的引用,这个引用本质上是一个地址值。当把对象作为实参传给方法时,值传递机制复制的是这个引用值,也就是把地址复制了一份给形参。形参持有的是同一个地址的副本,因此形参指向的对象和实参指向的对象是同一个对象,通过形参修改对象的属性,自然就反映到了这个共享的对象上。
但关键的区别在于:如果方法内部让形参引用指向了一个新的对象,这个操作只是改变了形参这个副本变量的指向,实参变量仍然指向原来的对象,外部看不到任何变化。这正是"Java 是值传递"这一结论的核心证据。考生只要抓住"复制的是引
本篇完!