以下是根据阅读cpp referrence得出来,加上一些自己的理解。
1:并不是默认参数的右边都必须是默认参数,而是在该作用域内右边参数已经有默认值。我觉得可以理解为给一个命名空间的的所有默认参数添加了一个集合,后面的默认参数可以使用之前的默认参数集。
#include<iostream>
void f(int, int, int = 10);
void f(int, int = 6, int);
void f(int = 4, int, int);
void f(int a, int b, int c) {
std::cout << a << b << c << std::endl;
}
int main() {
f();
return 0;
}
运行结果为:
但也必须保证重载函数右边的默认实参先声明,让编译器先可以右边参数的默认实参,如果改变前两个函数顺序,将会报错:缺少形参3的默认实参。
同一定义域内也不能重定义默认参数,如果将第二个函数改为f(int,int=6,int=8)也会报错
也可用命名空间验证,注意:using命令不仅仅会承接之前的默认参数集,还会及时更新之后的默认参数集合。
#include<iostream>
namespace N1 {
void f(int, int, int = 10);
}
using N1::f;
void g1() {
f(3, 1);
//此处只能见第三个默认参数,f(3),f()等调用都会报错
}
namespace N1 {
void f(int, int = 6, int);
}
void g2() {
f(3);
//using命令及时更新了默认实参集
}
namespace N1 {
void f(int = 5, int, int);
void f(int a, int b, int c) {
std::cout << a << b << c << std::endl;
}
}
int main() {
g1();
g2();
f();
return 0;
}
运行结果为
c++规定:对于非模板函数,当在同一作用域中重声明函数时,可以向已声明的函数添加默认实参。在函数调用点,可用的默认实参是由该函数所有可见的声明中所提供的默认实参的并集。重声明不能为已有可见默认值的实参引入默认值(即使值相同)。内层作用域中的重声明不从外层作用域获得默认实参。
#include<iostream>
void f(int, int = 10);
void m() {
//调用f(3,10)
f(3);
//内层定义域不能从外层得到默认参数。
//void f(int=3,int)
void f(int, int = 8);
//调用f(3,8)
f(3);
//同一定义域重定义
//void f(int, int = 7);
//调用f(9,8)
void f(int = 9, int);
f();
}
void f(int a, int b) {
std::cout << a << b << std::endl;
}
void f(int = 6, int);
int main() {
m();
//调用f(6,10)
f();
return 0;
}
结果为,符合预期。
注意,对于可变参数,可能出现以下情况:
#include<iostream>
template<typename... T>
void f(int a = 10, T... arg) {
std::cout << a << std::endl;
f(arg...);
}
int main() {
f();
return 0;
}
虽然默认参数右边没有默认参数,但是右侧是参数包,参数包可以展开,然后第一个就有默认参数了。所以也可以说符合上面规定,但是我觉得一般可变参数可能很少和默认参数一起使用,特别是一次只展开一个参数的情况,会造成死循环,就像上面这段代码一样。
2:默认实参只能用使用编译时不潜在求值并且已知具体值的量,不仅仅包括直接输入的值,还包括sizeof,静态变量等等。
#include<iostream>
class test {
public:
int a;
void f(int x = sizeof a) {
std::cout << x << std::endl;
}
void g(int x = b) {
std::cout << x << std::endl;
}
//静态变量定义位置无影响
static const int b = 10;
};
int main() {
test().f();
test().g();
return 0;
}
下面是gcc 13.1执行结果,vs上报错,应该没有完全遵守规则
但是,在涉及到虚函数继承的时候,虚函数的覆盖函数并不会继承该虚函数的默认实参
#include<iostream>
class base {
public:
virtual void f(int a = 10) {
std::cout << "base:" << a << std::endl;
}
};
class derival :public base {
public:
void f(int a) override {
std::cout << "derival:" << a << std::endl;
}
};
int main() {
std::unique_ptr<base> x{ new derival };
x->f();
return 0;
}
在vs的输出结果如下:
可能会有疑问,不是虚函数的默认实参不能继承吗,而这段代码明显调用的是子类的函数,为何也能得到默认实参10呢,其实不是虚函数的子函数得到了默认实参,而是因为默认实参调用应该根据对象的静态类型确定。不要忘了我们是利用多态调用子类函数,但是由于编译时x的静态类型为base,所以可以得到默认实参10。
可以理解为在编译代码的时候,编译器并不知道我们利用了多态,它只会认为x的类型为base,然后在编译f()函数的时候,根据base的f()函数是有默认实参的,所以会先用10将实参这个位置填起来,如果后面用户自己制定了实参将10覆盖就行,所以出现了多态时子类函数可以得到父类虚函数默认参数的现象。我觉得这种情况可能就是c++内部的矛盾之一吧,这在普通函数是完全符合默认实参的规定和需求的,但是多态调用时候明明规定虚函数的覆盖函数并不会继承该虚函数的默认实参,但实际上却因为编译的处理得到了父类的默认实参。
以上两点我觉得是默认实参最重要的两点规则,下面还有一些补充规则,其实更多是因为其他规则的限定导致的
1:对于非模板类的成员函数,类外的定义中允许出现默认实参,并与类体内的声明所提供的默认实参组合。
#include<iostream>
class test {
public:
test(int);
void f(int, int, int = 6);
};
void test::f(int a, int b = 6, int c) {
std::cout << a << b << c << std::endl;
}
test::test(int a = 7) {
std::cout << a << std::endl;
}
int main() {
test t;
t.f(3);
return 0;
}
程序输出结果
f()函数就是声明与定义的实参进行组合。
其实,上面这段代码其实有明显的错误,可能是vs没有严格遵从c++规范的原因导致程序能够正常运行,甚至没有警告出现。
c++规定,如果类外的默认实参会使成员函数变成默认构造函数或复制/移动 构造函数/赋值运算符,那么程序非良构。
c++规定的非良构就是程序拥有语法错误或可诊断的语义错误。遵从标准的 C++ 编译器必须为此给出诊断。gcc13.1也未给出报错,下面是clang 16.0.0编译器给出的报错信息。
error:addition of default argument on redeclaration makes this constructor a default constructor。
翻译一下就是在重新声明时添加默认参数会使此构造函数成为默认构造函数。
至于有些编译器能够运行的原因我觉得无非c++规定提供构造函数后,类不再提供默认构造函数。所以调用默认构造函数的时候编译器在构造函数表中找到了一个可以不需要参数的非默认构造函数,所以调用此函数,但毕竟我们没有定义默认构造函数,还是有失偏颇。
如果加上
test() = default;
vs编译器会报重载函数调用二义性和存在多个默认构造函数,将非默认构造函数视为默认构造函数,由此也能说明vs并未完全遵从c++标准。
c++也规定,对于类模板的成员函数,所有默认实参必须在成员函数的初始声明处提供。
#include<iostream>
template<class T>
class test {
public:
void f(T = 10);
};
template<class T>
void test<T>::f(T a) {
std::cout << a << std::endl;
}
int main() {
test<int> t;
t.f();
return 0;
}
如果写成
#include<iostream>
template<class T>
class test {
public:
void f(T);
};
template<class T>
void test<T>::f(T a = 10) {
std::cout << a << std::endl;
}
int main() {
test<int> t;
t.f();
return 0;
}
vs编译器会报错
其中有一条:函数不接受0个参数。这可能会很让人奇怪,明明定义时候写了默认参数,为什么会显示不接受0个参数。这是因为模板的编译和普通函数的编译不一样,模板编译只会看声明,等到具体调用的时候才会去看定义,所以模板如果在声明处没有表明默认参数,编译器就会认为该函数没有默认参数,也就出现了不接受0个参数这一错误。
但是,友元函数恰好相反,c++规定:如果friend声明指定了默认实参,那么它必须是友元函数定义,且该翻译单元中不能有此函数的其他声明。
#include<iostream>
class T {
public:
friend void f(int = 10);
};
void f(int a) {
std::cout << a << std::endl;
}
int main() {
T t;
f();
return 0;
}
上面这段代码在vs依旧能够成功运行,不过这次好歹报了错:友元声明无法向以前的声明添加默认参数。
下面是clang 16.0.0的报错
error: friend declaration specifying a default argument must be the only declaration。
error: friend declaration specifying a default argument must be a definition。
翻译就是指定默认参数的友元声明必须是唯一的声明,指定默认参数的友元声明必须是一个定义。
因为友元函数的声明只起到一个提供访问权限的作用,和函数本身的性质关系其实并不是很大。
当然,c++说的friend声明只是指类内的有friend字样的声明,而不是友元函数这个函数本身的声明不能提供默认参数,下面这样声明也是可以的。
#include<iostream>
void f(int = 10);
class T {
public:
friend void f(int);
};
void f(int a) {
std::cout << a << std::endl;
}
int main() {
T t;
f();
return 0;
}
2:c++规定:除了函数调用运算符外,运算符函数不能有默认实参。下面这段代码明显不行。
#include<iostream>
#include<vector>
class T {
public:
T(std::vector<int> a) :num(a) {};
int operator[](int a = 10) {
return num[a];
}
private:
std::vector<int> num;
};
int main() {
std::vector<int> a{ 1,2,3,4,5 };
T t(a);
std::cout << t.operator[]();
return 0;
}
3:c++规定:默认实参只能在函数声明和lambda表达式的形参列表中出现,而不能在函数指针、函数的引用,或在typedef声明中出现 。即以下行为只有f()和f4()可以成功运行。
#include<iostream>
void f(int, int = 10);
void f(int a, int b) {
std::cout << a << b << std::endl;
}
auto f4 = [](int a = 10) {
std::cout << a << std::endl;
};
int main() {
void(*f1)(int a = 10, int) = f;
void(&f2)(int a = 10, int) = f;
typedef void(*f3)(int a = 10, int);
f(1);
f1();
f2();
f3();
f4();
return 0;
}
4:c++规定:
如果一个inline函数在不同的翻译单元中声明,那么默认实参的累积集必须在每个翻译单元的结尾相同。
如果一个非 inline 函数在不同翻译单元的同一命名空间作用域中声明,那么对应的默认实参在存在时必须相同(但某些翻译单元中可以缺少一些默认实参)。
emmm,没怎么看懂,但是inline函数一般声明和定义是在一起的,所以应该用处影响不大。
5:c++规定:显示对象形参不能有默认实参。