0%

C++ 形参带默认值的函数

从一个 sum 函数说起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sum(int a = 10, int b = 20)
{
return a + b;
}

int main()
{
int a = 10;
int b = 20;

cout << sum(a, b) << endl;
cout << sum(a) << endl; // b 用默认值 20
cout << sum() << endl; // a、b 都用默认值
}

默认参数让调用方可以省略实参;被省略的实参,由编译器用对应的默认值补上。

其中有一些值得注意的点:

一、形参给默认值只能从右向左给

调用函数时,实参按位置对号入座:第一个实参给 a,第二个给 b,不能跳过。

1
2
3
4
int sum(int a, int b) { return a + b; }

sum(10, 20); // 10 给 a,20 给 b
sum(10); // 10 给 a,b 缺一个,报错

sum(10) 里的 10 只会给 a,不会跑到 b 那儿去。想给 b 传值,就必须先把 a 传了。C++ 也没有 sum(, 20) 这种留空位的写法。

所以调用时能省的,只能是最后面那几个实参。既然只能省尾巴,默认值也只能从尾巴开始给。

1
2
int sum(int a, int b = 20);  // ✅ 只传 a,b 用默认值
int sum(int a = 10, int b); // ❌ 编译不通过

第二种为什么不行?b 没有默认值,每次调用都必须传 b;要传 b 就得先传 a。结果 a 每次都有实参,它的默认值 10 永远用不上。这种默认值是句废话,C++ 不许这么写。

二、不同传参方式的效率差距

下面有四种不同的传参方式和相应的汇编代码(x86 Debug 构建,MSVC 风格):

1
int ret = sum(a, b);
1
2
3
4
5
mov eax, dword ptr [ebp-8]   ; 取 b内存上的值,存入寄存器
push eax ; 压栈
mov ecx, dword ptr [ebp-4] ; 取 a
push ecx ; 压栈
call sum
1
int ret = sum(a);
1
2
3
4
push 14H                     ; 不用取b内存上的值,直接把20压栈
mov ecx, dword ptr [ebp-4] ; 取 a
push ecx
call sum

sum(a) 和 sum(a, b) 相比,少了一条 mov:默认值 20 是编译期已知的常量,不需要先从内存里取出来。

1
int ret = sum(a, 40);
1
2
3
4
push 28H                     ; 和上面一样,直接压栈
mov ecx, dword ptr [ebp-4]
push ecx
call sum
1
int ret = sum();
1
2
3
push 14H                     ; b = 20
push 0AH ; a也不用取值了,直接压栈
call sum

对比上面四种方式:

调用 第二个实参从哪来 与 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
2
3
4
5
// 写法一:定义处给默认值
int sum(int a = 10, int b = 20)
{
return a + b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 写法二:声明处给默认值
int sum(int a = 10, int b = 20);

int main()
{
cout << sum() << endl;
}

// 定义处不能再写默认值
int sum(int a, int b)
{
return a + b;
}

但同一个形参的默认值,在同一作用域的声明和定义中只能写一次:

1
2
int sum(int a, int b = 20);
int sum(int a, int b = 20); // ❌ 重定义默认参数,值一样也不行

工程上的习惯是:默认值写在头文件的声明里,.cpp 的定义处不再重复。

四、多条声明要按顺序补默认值

函数可以声明多次,但把声明和定义按出现顺序从上往下读,每一处新增的默认值都必须满足“它右边的形参都已经有默认值”。

先给右边,再给左边,合法:

1
2
int sum(int a, int b = 20);  // 第一次声明:给 b
int sum(int a = 10, int b); // ✅ 第二次声明:给 a(b 上面已经给过了)

反过来先给左边,不合法:

1
2
int sum(int a = 10, int b);  // ❌ 此时 b 还没有默认值
int sum(int a, int b = 20);

结论就是:声明按出现顺序从上往下读,默认值从右往左补,每个形参只能给一次。

小结

  • 默认值从右向左给,右边所有形参都有默认值,左边才能有。
  • 缺省实参由编译器在调用点填好,函数体不判断参数是否被省略,运行期没有额外分支。
  • 传变量时,省略一个参数会少一条 mov 取值指令;传常量则和传默认值没有差别。
  • 声明处、定义处都能给默认值,但同一作用域内同一个形参只能出现一次。
  • 多次声明时,声明按出现顺序从上往下读,默认值从右往左补。