从一个 sum 函数说起
1 | int sum(int a = 10, int b = 20) |
默认参数让调用方可以省略实参;被省略的实参,由编译器用对应的默认值补上。
其中有一些值得注意的点:
一、形参给默认值只能从右向左给
调用函数时,实参按位置对号入座:第一个实参给 a,第二个给 b,不能跳过。
1 | int sum(int a, int b) { return a + b; } |
sum(10) 里的 10 只会给 a,不会跑到 b 那儿去。想给 b 传值,就必须先把 a 传了。C++ 也没有 sum(, 20) 这种留空位的写法。
所以调用时能省的,只能是最后面那几个实参。既然只能省尾巴,默认值也只能从尾巴开始给。
1 | int sum(int a, int b = 20); // ✅ 只传 a,b 用默认值 |
第二种为什么不行?b 没有默认值,每次调用都必须传 b;要传 b 就得先传 a。结果 a 每次都有实参,它的默认值 10 永远用不上。这种默认值是句废话,C++ 不许这么写。
二、不同传参方式的效率差距
下面有四种不同的传参方式和相应的汇编代码(x86 Debug 构建,MSVC 风格):
1 | int ret = sum(a, b); |
1 | mov eax, dword ptr [ebp-8] ; 取 b内存上的值,存入寄存器 |
1 | int ret = sum(a); |
1 | push 14H ; 不用取b内存上的值,直接把20压栈 |
sum(a) 和 sum(a, b) 相比,少了一条 mov:默认值 20 是编译期已知的常量,不需要先从内存里取出来。
1 | int ret = sum(a, 40); |
1 | push 28H ; 和上面一样,直接压栈 |
1 | int ret = sum(); |
1 | push 14H ; b = 20 |
对比上面四种方式:
| 调用 | 第二个实参从哪来 | 与 sum(a, b) 相比 |
|---|---|---|
sum(a, b) |
变量 b,先 mov 再 push |
基准 |
sum(a) |
默认值 20,直接 push 14H |
少一条 mov |
sum(a, 40) |
常量 40,直接 push 28H |
少一条 mov,与 sum(a) 相同 |
sum() |
两个默认值,直接 push |
少两条 mov,与 sum(10, 20) 相同 |
由此能得到几个结论:
- 与传变量相比,传默认值和传常量会省下从内存取值的
mov指令,调用量大时能提高效率。 - 默认值在调用点由编译器补成实参:
sum(a)和sum(a, 20)传给函数的参数完全一样,函数体里没有“b 到底传没传”的判断。
三、形参默认值写在声明处或定义处都行,但只能写一次
两种都可以:
1 | // 写法一:定义处给默认值 |
1 | // 写法二:声明处给默认值 |
但同一个形参的默认值,在同一作用域的声明和定义中只能写一次:
1 | int sum(int a, int b = 20); |
工程上的习惯是:默认值写在头文件的声明里,.cpp 的定义处不再重复。
四、多条声明要按顺序补默认值
函数可以声明多次,但把声明和定义按出现顺序从上往下读,每一处新增的默认值都必须满足“它右边的形参都已经有默认值”。
先给右边,再给左边,合法:
1 | int sum(int a, int b = 20); // 第一次声明:给 b |
反过来先给左边,不合法:
1 | int sum(int a = 10, int b); // ❌ 此时 b 还没有默认值 |
结论就是:声明按出现顺序从上往下读,默认值从右往左补,每个形参只能给一次。
小结
- 默认值从右向左给,右边所有形参都有默认值,左边才能有。
- 缺省实参由编译器在调用点填好,函数体不判断参数是否被省略,运行期没有额外分支。
- 传变量时,省略一个参数会少一条
mov取值指令;传常量则和传默认值没有差别。 - 声明处、定义处都能给默认值,但同一作用域内同一个形参只能出现一次。
- 多次声明时,声明按出现顺序从上往下读,默认值从右往左补。