函数默认参数 #
- 声明的时候带默认参数,定义的时候可以不带。但是不能定义的时候有默认参数而声明的时候没有。
- 如果用默认参数,那么必须从右到左都有(左边可以没有,不能中断)。
- 传入参数的时候,只能从左往右传入,未传入的自动使用默认的参数。
模板 #
定义:
template<typename T> T foo(T a) {
//your function
}
如果还有一个同名的非模板函数:
int foo(int a) {
// function
}
调用的时候:
- 如果使用
foo(),那么会直接调用非模板的函数。 - 如果用
foo<>(),那么会自动推断,并从模板实例化函数。如果指定类型,则直接从模板按指定的类型实例化函数。
在两个不同的模板参数时,可以定义模板返回用auto关键字,让编译器自动推导返回类型。这是C++17后的CTAD(class template auto deduction)。
注意: CTAD仅用于推断实参(argument,调用时) 不能推断形参(parameter,函数声明)
在C++20后,函数参数传入auto会自动变成模板。
非类型模板:定义constexpr或者consteval(C++20),不写 typename 。
template <int N>
auto foo() {
std::cout<<N;
}
foo<5>(); // print 5
由于模板展开需要看到定义,所以模板函数应该放在头中。模板不违反单一定义原则。
左值和右值 #
- 左值表达式:在表达式结束之后,还能用标识对象访问的表达式。
- 右值表达式:表达式结束之后立即销毁的临时量。
特别:++x是左值,x++是右值。
引用 #
左值引用 #
左值引用只能绑定到可修改的左值。
常量左值引用可以绑定到可修改/不可修改的左值,右值。
一个常量引用可以引用非常量的对应对象,这使得此引用由编译器保证不修改其引用的对象,而且是函数的只读参数的推荐做法。
如果类型不匹配,引用会尝试将其进行隐式类型转换,得到一个临时对象。需要注意:
- 只允许数值提升和非窄化转换。
- 此时转化后的临时对象会被引用持有,生命周期与引用本身相同。
- 修改原始对象不会对因引用产生的临时对象造成影响。
小心!引用无法延长函数返回值的声明周期,以下例子为UB:
const int& a = getTempVar();
引用和指针 #
左值引用可以绑定到右值,但是必须是常量的引用才可以。同时,对于右值的常量引用会使得直接绑定的临时对象生命周期被延长。
而右值引用(&&)则是用于移动语义。
与auto相结合 #
- 顶级
const: 指的是修饰此别名/对象的const - 低级
const: 指的是被引用绑定/被指针指向的const
指针可以同时拥有两种const,但是引用不行,因为引用不是对象。
当用auto关键字以及引用来初始化对象时,遵守:
- 引用会被删除。
- 顶层的常量会被删除。
当然可以用auto&来重新添加引用,不过这样会同时保留顶级的const。我们一般推荐要手动加上const,这样会让语义更加清晰。
constexpr不是表达式的一部分,所以auto不会自动推断出它,如果需要则要在auto前面手动加上,就像这样:
constexpr const auto& ref{getConstRef()};
但是推断指针的时候,auto会完整的保留指针的类型。
有一点比较特殊,使用const auto和auto const是同义的,都会使得新对象成为一个常量的指针。但是如果你想const auto const,那就会报错。
如果想要这么做,最好明确一点,就像这样:
const auto*auto* constconst auto* const
这样就很明显的看出,在*左边的意义是指向常量,在右边的意义是常量的指针,事实上既然都auto了,也不差这几个关键字吧?我其实比较担心auto不够智能这个问题。
为什么呢?
因为指针是一个对象,而引用可以理解成一个别名,所以当我们对引用进行推断的时候,实际上是推断原始的对象究竟是什么类型。
in/out 参数 #
为了避免拷贝,可以预先初始化一个对象传入其引用,这样可以避免拷贝,不过使得参数列表很乱。如果可行,还是通过返回值优化或者移动语义。