C语言不支持函数重载,也不支持函数参数的默认值,这既可以说体现了C语言的简陋,也可以说是避免了很多麻烦。 C++的函数参数支持默认值的机制就比较烦人,因此需要整理一下。

为了提高代码的可读性,C++尽量也不要使用函数默认值,在讨论的最后提供了几种简单的方式可以替代。

基础

C++支持给函数参数提供默认值,例如

1
2
3
4
5
6
7
8
9
10
void func(int a, int b=1){
...
}

int main(){
func(1,2);
func(3); // == func(3,1), a=3,b=1

return 0;
}

C++要求参数的默认值必须从右向左连续地提供,保证无默认值的参数不能出现在有默认值的参数的右侧,否则编译器无法判断参数缺省时的对应关系,这是语法错误,例如

1
2
3
4
5
6
7
void func(int a, int b=1, int c){ // compile error
...
}

void func(int a, int b=1, int c=2){ // ok
...
}

声明or定义

C++的函数可以单独进行声明和定义,还可以重复声明,此时如何处理参数的默认值是我们关注的重点。

首先需要明确的是,如果我们同时在声明和定义中添加默认值,即使两处提供的默认值是一样的,放在一起也是编译错误。

1
2
3
void func(int a, int b = 0);

void func(int a, int b = 0) { std::cout << "a = " << a << " b = " << b; } // compile error

如果两处提供的默认值不一样,当然更是编译错误,其实这里还涉及到参数默认值何时求值的问题,见下文。

第一种语法上允许的做法是:在声明中提供默认值,在定义中不提供默认值。例如

1
2
3
4
5
void func(int a=10);

void func(int a){
...
}

第二种语法上允许的做法是:在声明中不提供默认值,在定义中提供默认值。例如

1
2
3
4
5
void func(int a);

void func(int a=10){
...
}

这两种做法虽然语法上都可以编译通过,但是在实际使用中并不等价,即使目前函数的声明和实现还是在同一个文件中,两者也是有区别的,例如

1
2
3
4
5
6
7
8
9
void func(int a);

int main(){
func(); // compile error
}

void func(int a=10){
...
}

这里虽然在函数定义中出现了默认值,但是在函数使用之前编译器只见过函数声明,在函数声明中并没有找到默认值,因此编译器会报错。

一个更复杂的情景是将它们拆分为头文件和多个源文件,我们在main.cpp中使用func这个函数

1
2
3
4
5
6
7
// main.cpp
#include "func.h"

int main(){
func(2);
func();
}

如果采用第一种做法,在声明中提供默认值

1
2
3
4
5
6
7
8
9
10
// func.h

void func(int a=10);

// func.cpp
#include "func.h"

void func(int a){
...
}

可以编译通过并顺利运行,即使将func.cpp单独编译为一个库,也是可以正常编译运行的,因为默认值在接口头文件里。

如果采用第二种做法,在定义中提供默认值

1
2
3
4
5
6
7
8
9
10
// func.h

void func(int a);

// func.cpp
#include "func.h"

void func(int a=10){
...
}

那么编译main.cpp时,func(2)语句可以正常编译,但是func()语句会报错,因为编译器在处理它的时候并不知道参数默认值是什么, 默认值只在另一个编译单元(库)中!

结合我们的实践需求来对比这两种做法:

  • 在单个源文件中使用函数前置声明的主要目的就是在定义函数之前就可以使用函数,如果将默认值藏在后面的函数定义里,就直接违背了这个意愿。
  • 对于库文件更是如此,如果仅仅将默认值放在具体实现的源文件的函数定义中,在对外提供的接口头文件的函数声明中不提供默认值,那么这个默认值实际上就不能被库的使用者所知晓和使用,这也违背了提供函数参数默认值的意愿。

因此普遍的做法是:在函数声明以及库的接口头文件中提供默认值,在函数定义中不提供默认值。

C++允许重复进行函数声明,如果考虑到函数声明中提供默认值,在语法上允许的做法是: 在一个函数声明中提供参数默认值,在其它的函数声明中不提供默认值,唯一提供默认值的函数声明的出现顺序是不重要的,例如

1
2
3
void func(int a, int b);
void func(int a, int b = 0);
void func(int a, int b);

下面的做法则由于在声明中多次出现默认值,会导致语法错误

1
2
void func(int a, int b = 0);
void func(int a, int b = 0); // compile error

如果多个函数参数都具有默认值,在提供多个函数声明时,甚至还支持将不同参数的默认值分散放置在不同的函数声明中,例如

1
2
3
4
5
void func(int a,int b,int c,int d);
void func(int a,int b,int c,int d=3);
void func(int a,int b,int c=2,int d);
void func(int a,int b=1,int c,int d);
void func(int a,int b,int c,int d);

这一组声明只相当于下面的一个声明

1
void func(int a,int b=1,int c=2,int d=3);

参数默认值在函数的多次声明中被累积。

小结

仅从语法的角度,虽然函数可能有若干个函数声明和一个函数定义,但是在使用参数默认值时, 同一个参数的默认值只允许在其中出现一次,即使我们在不同的位置提供了相同的默认值,在语法是也是不允许的

1
2
void func(int a=10);
void func(int a=10);

要么只在函数定义中提供,要么只在某一个函数声明中提供。考虑到实践中的需求,建议在某一个函数声明中提供参数默认值。

不同默认值

我们可以利用C++函数参数默认值的求值时机,以及函数签名不含默认值的特性,实现对同一个函数提供不同的默认值。

第一种情况

C++的函数参数默认值是在函数调用时才计算的,在使用自定义类型或者全局变量提供默认值时,我们无法保证不同时刻的调用会得到相同的默认值,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int s = 0;

void func(int a = s);

int main() {
func(); // func(0);
s = 100;
func(); // func(100);

return 0;
}

void func(int a) { std::cout << " a = " << a << "\n"; }

这里的函数参数默认值就是不一样的,对于自定义类型同理。除了使用全局变量,我们甚至可以通过函数调用来生成默认值,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int create() {
static int m = 0;
return m++;
}

void test(int arg = create()) {
std::cout << "call test(" << arg << ")\n";
}

int main() {
for (int i = 0; i < 10; i++) { test(); }

return 0;
}

在函数调用过程中,除了参数默认值的求值时机,其实还有普通实参的求值时机,多个实参求值的相对顺序实际上是未定义行为,但是通常会从右向左计算。

第二种情况

C++为了支持函数重载,将函数的参数列表信息也视为函数签名的一部分,用特定的规则生成唯一的函数标识符, 但是函数参数默认值并不被包括在函数标识符中,这给我们留下了灵活的操作空间: 虽然在同一个编译单元中不允许默认值出现两次(无论默认值是否一致),但是不同的编译单元中使用不同的默认值却是允许的,因为编译器根本不知道在别的编译单元中函数的默认值信息,在最后链接阶段编译器只能拿到函数标识符。

例如下面的例子我们在不同的编译单元内对同一个函数进行了声明,但是在声明中提供了不同的默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// main.cpp
void func(int a = 10);
void test();

int main() {
func();
test();

return 0;
}

// func.cpp
#include <iostream>

void func(int a = 20);

void func(int a) { std::cout << "a = " << a << "\n"; }

void test() { func(); }

可以成功编译运行,执行的结果如下,两个编译单元内采用了不同的默认值。

1
2
a = 10
a = 20

补充

建议在C++编程中不要提供参数默认值,可以使用函数重载进行替代,例如

1
2
3
4
5
6
7
int func(int a, int b){
...
}

int func(int a){
return func(a,1); // default b = 1
}

更推荐的方式是不使用函数重载,直接在函数名上体现默认值信息,提高代码的可读性,例如

1
2
3
4
5
6
7
int func(int a, int b){
...
}

int func_b1(int a){
return func(a,1); // default b = 1
}