本节摘要:调用方与被调方的参数交接有四种经典语义:传值(复制一份)、传引用(传地址)、传值结果(复制进出各一次)、传名(每次使用重新求值)。本节用同一个 swap 案例跑四种方式,推演各自的结果差异,并说明主流语言的选择与背后取舍。
阅读完本节,你应当能够:
案例只有一个,双重交换:
被调方:procedure swap(a, b) t = a a = b b = t 调用方:i = 2; j = 3 swap(i, j) print i, j ← 四种方式各打印什么? 追加考题:swap(i, i) ← 别名调用,结果更分化和 swap(i, j) 后的 i、j 一起看
方式一:传值。调用时把 i、j 的值复制给 a、b;swap 交换的是副本;返回后 i、j 原封不动。结果:2 3——交换失败,这是 C 默认行为,也是"为什么 C 写 swap 必须传指针"的答案。
方式二:传引用。调用时传 i、j 的地址;a、b 是 i、j 的别名;交换别名就是交换本体。结果:3 2——成功。但别名调用 swap(i, i) 时翻车:a、b 是同一变量的两个名字,交换等于自交换,i 保持 2(有的实现按顺序执行,t=a 后 a=b 再 b=t,三个动作全作用在同一格子上,值不变)。
方式三:传值结果(拷入拷出)。进入时复制(同传值),返回时把形参的终值复制回实参(传引用的效果),但复制发生在调用结束时刻。结果:3 2——成功;但 swap(i, i) 结果与传引用不同:进入时 a、b 各拿到 2 的副本,交换后 a=2、b=2,返回时依次写回 i 两次,i 终值 2。中间态可见的场合(并发、重入)它与真引用行为不同。
方式四:传名。实参表达式以"每次用到才求值"的方式塞进被调方——效果近似宏展开。对 swap 这类简单调用,结果同传引用 3 2;但一旦实参带副作用或下标计算,行为分化:swap(i, a[i]) 在传名下每次对 a 或 b 赋值都重新计算下标,若交换过程中 i 变了,写回的元素就换了位置。传名是历史产物(Algol 60 的定义),现代语言几乎绝迹,其思想活在惰性求值与宏系统里。
四种方式的结果汇总成一张决策速查:
调用 swap i j 的打印结果: 传值 → 2 3 交换失败,调用方毫发无损 传引用 → 3 2 交换成功 传值结果 → 3 2 交换成功,但中间态可见的场合与真引用不同 传名 → 3 2 简单场合同引用,副作用场合行为放大 调用 swap i i 的对照(别名调用): 传值 → 2 2 无影响 传引用 → 2 2 自我交换,值不变(按序执行语义) 传值结果 → 2 2 副本各自为 2,写回两次同一值 传名 → 2 2 同引用 真正的分化藏在带下标与副作用的调用里,四种方式四种行为

除了传递方式,还有一个常被忽略的约定:多参数时先算哪个实参。传值类语言里 f(g(), h()) 的 g、h 执行顺序,多数语言规范故意不定义(C 与 C++ 皆为未指定),留编译器自由。若 g、h 有共享副作用,未指定顺序就等于可移植性陷阱——同一份代码换个编译器结果不同。Java 与 Python 规定了从左到右,消除了这个不确定性。写跨平台代码的纪律:实参表达式不要有相互依赖的副作用。
| 语言 | 策略 | 备注 |
|---|---|---|
| C | 传值 | 想改本体就显式传指针(仍是传值,传的是地址值) |
| C++ | 传值 + 引用 | 引用是语言级传引用 |
| Java | 传值 | 对象变量传的是"引用的值",常被误解为传引用 |
| Python | 传对象引用 | 名字绑定被重新赋值不影响调用方,可变对象内容可改 |
| Fortran | 传引用 | 历史传统,编译器可借此优化 |
Java 那一行值得多说一句:obj.set(x) 能改对象内容(调用方可见),obj = new Obj() 不影响调用方——前半像引用、后半像传值,统一解释只有一种:传递的是"引用的副本"。
⚠️ 常见坑:把"传对象引用"当"传引用"用,在被调方对参数重新赋值期待调用方可见。Python 与 Java 里
def f(a): a = 新对象之后调用方毫无感觉——改的是副本绑定。要影响调用方,只能改对象内容或返回新值。
💡 关键直觉:四种方式的本质区别是"被调方拿到的是什么"——值副本、地址、延迟求值的表达式、还是带写回协议的副本。设计语言选哪种,是在"安全(隔离副作用)"与"表达力(能改调用方状态)"之间落子。
问:结构体这样的大参数怎么传? 按约定分级:小到一两个寄存器装得下,拆开进寄存器;中等大小,调用方在栈上开一块传地址或传值;很大,几乎都传指针。约定的细节各指令集不同,但思路一致——参数交接的成本要和体积挂钩,无脑传值会把栈当货车用。
问:可变参数(如 printf 的省略号)怎么实现? 前几个固定参数走约定寄存器,后续参数统一压进栈上连续区域,被调方靠格式字符串自行解读类型与个数。约定只保证参数在哪,不保证参数是什么——类型安全完全交给调用方自觉,这也是可变参数函数容易出错的机理根源。
补一问:返回值算参数传递吗? 算逆向的传递。小返回值走约定寄存器直接带回;大返回值(结构体)由调用方在栈上开一块、把地址偷偷作为隐含首参数传给被调方,被调方往里填——大返回值本质是传地址的变体。理解了参数的交接,返回值不过是同一台戏的倒放。
交接方式定了,下一节看交接的物理现场:活动记录的六件组成、调用与返回的指令序列。