函数的默认参数
简介
默认实参是指在函数声明或定义中为参数提供一个默认值,这样在调用函数时,如果用户没有提供该参数的值,函数将使用这个默认值。例如,在函数声明中,可以这样写:void f(int a = 5);
,其中 5
就是 a
参数的默认实参。这意味着如果在调用 f
时没有指定 a
的值,它将默认为 5
。
基本概念
- 作用: 提供了一种机制,使得函数的使用者可以不提供某些参数的值,而函数仍然能够正常运行。
- 位置: 默认实参必须位于函数参数列表的尾部,除非是形参包(参数包)的情况。
规则与限制
尾部限制: 默认实参只能出现在参数列表的尾部,不能在中间。
局部变量和this指针: 默认实参不能使用局部变量、this指针或非静态成员变量的值。
模板函数: 对于模板函数,所有默认实参必须在模板的初始声明中提供。
使用场景
- 简化调用: 当函数提供了多个参数,但用户可能不需要全部时,提供默认实参可以简化调用。
- 修改代码: 在不干扰现有代码的情况下,添加新功能时,可以通过添加默认实参来扩展接口。
虚函数与默认实参
静态绑定: 虚函数的默认实参值是静态决定的,不会因为动态类型而改变。这意味着通过基类指针调用虚函数时,会使用基类中的默认实参值,即使派生类重写了该函数并提供了不同的默认值。
类成员函数中的默认实参
- 类体内声明: 成员函数的默认实参可以在类体外的定义中出现,但不能重新定义。
- 构造函数: 对于非模板类,成员函数的默认实参可以与类体内的声明组合,但不能用于构造函数、复制/移动构造函数或赋值运算符。
默认模板实参
- 模板参数: 可以为模板参数定义默认值,这些默认值可以用于函数模板和类模板。
- 递归使用: 默认模板实参可以是前面模板参数的函数或类型。
变参函数
- 不适用情况: 变参函数中,省略号部分通常不被视为具有默认实参。
通过这些解释,我们可以看到默认实参是C++中一个灵活且强大的特性,但同时也有一些规则和限制需要遵守。正确使用默认实参可以提高代码的可读性和灵活性,但不当使用可能会导致意外的行为。
示例代码
#include <iostream>
using namespace std;
void TestFunc01(int a = 10, int b = 20) {
cout << "a + b = " << a + b << endl;
}
//注意点:
//1. 形参b设置默认参数值,那么后面位置的形参c也需要设置默认参数
void TestFunc02(int a, int b = 10, int c = 10) {}
//2. 如果函数声明和函数定义分开,函数声明设置了默认参数,函数定义不能再设置默认参数
void TestFunc03(int a = 0, int b = 0);
void TestFunc03(int a, int b) {}
int main(int argc, char *argv[]) {
//1.如果没有传参数,那么使用默认参数
TestFunc01();
//2. 如果传一个参数,那么第二个参数使用默认参数
TestFunc01(100);
//3. 如果传入两个参数,那么两个参数都使用我们传入的参数
TestFunc01(100, 200);
return 0;
}
程序输出:
a + b = 30
a + b = 120
a + b = 300
注意
- 函数的默认参数从左向右,如果一个参数设置了默认参数,那么这个参数之后的参数都必须设置默认参数。
- 如果函数声明和函数定义分开写,函数声明和函数定义不能同时设置默认参数。
函数的占位参数
C++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
#include <iostream>
using namespace std;
void TestFunc01(int a, int b, int) {
//函数内部无法使用占位参数
cout << "a + b = " << a + b << endl;
}
//占位参数也可以设置默认值
void TestFunc02(int a, int b, int = 20) {
//函数内部依旧无法使用占位参数
cout << "a + b = " << a + b << endl;
}
int main(int argc, char *argv[]) {
//错误调用,占位参数也是参数,必须传参数
//TestFunc01(10,20);
//正确调用
TestFunc01(10, 20, 30);
//正确调用
TestFunc02(10, 20);
//正确调用
TestFunc02(10, 20, 30);
return 0;
}
程序输出:
a + b = 30
a + b = 30
a + b = 30
什么时候用,在后面我们要讲的操作符重载的后置++要用到这个。
函数形参包
函数形参包是指在C++中,允许函数接受一个可变数量的参数的机制。具体来说:
函数形参包的具体含义:
函数形参包是指在函数定义或声明中使用模板参数包(template parameter pack)来表示可以接受零个或多个类型相同的参数。这种机制允许函数在编译时动态地处理不同数量的参数。
语法和用法:
- 语法:函数形参包通常以
typename...
或class...
的形式出现在函数模板的形参列表中。 - 用法:通过递归或展开技术,可以对形参包中的每个参数进行处理。例如,递归方法允许逐个处理形参包中的每个参数,直到形参包为空。
与默认实参的关系:
- 在给定的原文中,函数形参包可以作为一种例外情况,允许在某些形参具有默认实参的情况下,其后的形参不具有默认实参。这是因为形参包的展开机制允许编译器在编译时动态处理参数数量,从而避免了对每个形参都必须指定默认值的要求。
优点:
- 类型安全性:函数形参包允许编译期类型检查,避免了
va_list
等变长参数机制中的类型不安全问题。 - 灵活性:函数形参包使得函数可以处理任意数量的参数,而不需要在函数定义时硬编码参数数量。
示例:
CPPtemplate <typename... Ts>
void print(Ts... args) {
(std::cout << ... << args) << std::endl;
}
在这个示例中,print 函数可以接受任意数量和类型的参数,并使用折叠表达式 (... << args
) 来输出所有参数。
总结:
函数形参包提供了一种强大的机制来处理可变数量的参数,不仅提高了代码的灵活性,还增强了类型安全性。这种机制在C++11及以后的版本中被广泛使用,尤其是在需要处理变长参数的场景中。
测试
你真的清楚默认实参吗?
我们将用一个一个例子和题目为你说明默认实参的所有行为,本文不是为没有使用过默认实参的开发者准备的。
第一题
拥有默认实参的形参必须位于函数的右侧是最后一个形参或旁边是别的默认实参???
实际上这条口口相传的规则很不严谨,我们看以下代码
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 << '\n'; }
int main(){
f();//4 6 10
}
- 结论:运行不会有任何问题。
- 原因:在函数声明中,所有在拥有默认实参的形参之后的形参必须拥有在这个或同一作用域中先前的声明中所提供的默认实参。 你可能觉得很绕,其实说白了就是说,你可以给任何形参默认实参,但你需要在当前作用域提前给这个形参后面的形参默认实参。 比如这样是不行的:cpp第二个形参拥有默认实参没问题,但是在此之前,它后面的第三个形参必须也拥有默认实参才可以,我们只是调换了一下位置,让它作为第一个函数声明,也就错了。
void f(int, int = 6, int); void f(int, int, int = 10); void f(int = 4,int,int);
- 有例外情况是:除非该形参是从某个形参包展开得到的或是函数形参包,如下:cpp
template<class...T> struct X { void f(int n = 0, T...) { std::cout << n << '\n'; }; }; template<class...Args> void f_(int n = 6, Args...args) { } int main(){ X().f(); X<int>().f(1, 2);//实例化了X::f(int n=0,int) }
第二题
以下代码哪些正确哪些错误?
class C
{
void f(int i = 3);
void g(int i, int j = 99);
C(int arg); // 非默认构造函数
};
void C::f(int i = 3) {}
void C::g(int i = 88, int j) {}
C::C(int arg = 1) {}
- 结论:
C::f
和C::C
都错误 - 原因:对于非模板类的成员函数,类外的定义中允许出现默认实参,并与类体内的声明所提供的默认实参组合。如果类外的默认实参会使成员函数变成默认构造函数或复制/移动(C++11 起)构造函数/赋值运算符,那么程序非良构。对于类模板的成员函数,所有默认实参必须在成员函数的初始声明处提供。
C::f
看一眼也知道重定义默认实参了,默认实参已经在类作用域指定,C::C
添加默认实参是让它变成了默认构造函数,所以错误。C::g
就是正常的和第一题一样的组合,没什么问题。
第三题
这个打印结果如何解释?
struct Base{
virtual void f(int a = 7) { std::cout << "Base " << a << std::endl; }
};
struct Derived : Base{
void f(int a) override { std::cout << "Derived " << a << std::endl; }
};
int main(){
std::unique_ptr<Base>ptr{ new Derived };
ptr->f();//Derived 7
}
解释:虚函数的覆盖函数不会从基类定义获得默认实参,而在进行虚函数调用时,默认实参根据对象的静态类型确定
如果你不知道什么是静态类型,我们可以介绍一下
静态类型
对程序进行编译时分析所得到的表达式的类型被称为表达式的静态类型。程序执行时静态类型不会更改。
动态类型
如果某个泛左值表达式指代某个多态对象,那么它的最终派生对象的类型被称为它的动态类型。
// 给定
struct B { virtual ~B() {} }; // 多态类型
struct D: B {}; // 多态类型
D d; // 最终派生对象
B* ptr = &d;
// (*ptr) 的静态类型是 B
// (*ptr) 的动态类型是 D
对于纯右值表达式,动态类型始终与静态类型相同。
第四题
以下代码是否正确,会打印什么,为什么?
int main(){
int f = 0;
void f2(int n = sizeof f);
f2();
}
void f2(int n) {
std::cout << n << '\n';
}
结论:正确,打印8
原因:默认实参中能在不求值语境使用局部变量,
sizeof
显然是不求值的,没有任何问题,但是msvc不行。cppint main() { int f; void f2(int a = 0, int n = sizeof a); f2(); } void f2(int a,int n) { std::cout << a <<' '<< n << '\n'; }
没有任何问题,默认实参在不求值语境中能使用局部变量和之前的形参,但是msvc依旧不可以,它并没有遵守规定
第五题
以下代码哪些错误?
struct X {
int n = 6;
static const int a = 6;
void f(int n = sizeof + n) { std::cout << n << '\n'; }
void f_(int n = a) { std::cout << n << '\n'; }
};
int main(){
X().f();
X().f_();
}
结论:显然是成员函数f错误了
原因:默认实参中不能使用非静态的类成员(即使它们不被求值),除非用于构成成员指针或在成员访问表达式中使用。
所以就算我们是不求值的语境,一样不行
总结
- 给默认值的时候,从右向左给。
- 声明和定义都可以给,但是不能同时设置默认参数。建议在声明的时候设置默认参数。
- 调用效率高,因为编译器可以直接使用默认参数。减少了一条汇编指令。
- 占位参数,在函数内部无法使用。
有些是省略了的,没有提,如果要详细了解可以看 cppreference