17.1.5 函数调用方式
函数调用是本章高频点。考试最爱问:函数内部修改形参,函数外的实参到底变不变。答案取决于参数传递方式。
函数调用的基本角色
函数声明通常包括返回类型、函数名、参数列表和函数体。参数列表里的变量叫形参,调用时传入的值或变量叫实参。
text
int add(int x, int y) {
return x + y;
}
add(a, b)这里 x、y 是形参,a、b 是实参。
传值调用
传值调用把实参的值复制给形参。函数内部修改的是副本,不会影响调用者手中的原变量。
| 调用前 | 函数内操作 | 调用后 |
|---|---|---|
a=3, b=4 | swap(x, y) 中交换 x 和 y | a=3, b=4 |
本质上,形参和实参是两个存储单元,只是开始时值相同。
引用/地址调用
引用调用或地址调用把变量的位置传给函数,形参可以访问实参所在的存储单元。函数内部修改目标对象,调用者能看到变化。
| 调用前 | 函数内操作 | 调用后 |
|---|---|---|
a=3, b=4 | 通过引用或指针交换目标值 | a=4, b=3 |
这也是为什么引用调用的实参通常必须是可被赋值的对象,不能随便写成一个表达式。表达式 a+b 没有稳定的可写存储位置,不能作为普通引用参数的目标。
调用栈:函数为什么能回来
函数调用时,系统会在调用栈中保存返回地址、参数、局部变量等信息。嵌套调用和递归调用能正确返回,就是因为每次调用都有自己的活动记录。
mermaid
flowchart TB
A["main 调用 f"] --> B["压入 f 的活动记录"]
B --> C["f 调用 g"]
C --> D["压入 g 的活动记录"]
D --> E["g 返回,弹出 g"]
E --> F["f 返回,弹出 f"]递归并不神秘,它只是函数调用自己。每一层递归都占用一份栈空间,所以必须有结束条件,否则会无限递归或栈溢出。
解题方法
- 先判断是传值还是引用/地址。
- 标出形参是否有独立副本。
- 如果是引用/地址,继续判断修改的是指针本身还是指向的对象。
- 最后看函数返回后调用者变量是否被改变。
小练习
题:函数 swap(x, y) 内部交换了 x、y,若调用方式为传值,调用者的两个变量会交换吗?
答:不会。传值调用只交换形参副本,实参不受影响。
自查
- 传值调用和引用调用的根本差别是什么?
- 为什么递归需要调用栈?
- 引用参数为什么通常不能传入普通表达式?