1. auto
auto 关键字可以根据初始化值自动推导所定义变量的数据类型,其作用于编译阶段。 auto 通常用于迭代器的定义,因为其具体数据类型由于模板的原因可能十分冗长,使用 auto 可以使得代码更加简洁。 auto 可能造成一定的代码阅读理解困难,所以如果该变量的类型不是显而易见的最好不要使用。 auto 不能作为函数的参数类型,但在 C++14 中可作为返回类型。 auto 也不能作为类普通数据成员的数据类型,除了 (const static) 变量,并且必须在类内初始化。
2. decltype
decltype 关键字可通过括号内的表达式自动推导结果的数据类型,但并不会执行该表达式,同样作用于编译阶段。与 auto 相比其不需要对变量进行初始化,可用于变量的声明。
int a = 1;
decltype(a) b = 2;
3. nullptr
C++11 中使用 nullptr 来代替 NULL,避免不同系统对 NULL 具体数值的不同定义可能带来的问题,以及 NULL 与整型数据类型可能存在的二义性问题。
4. constexpr
constexpr 即 const expression,主要是告诉编译器其所修饰的变量、函数的结果在编译期就可确定值,可以进行相应的优化以节省内存和时间。而 const 只是表示变量被初始化后不能再修改,因此可能在运行期才能确定值,这时编译器为了安全可能不会对 const 进行优化。
实际上,对于变量和函数,constexpr 有着不同的行为。constexpr 变量必须初始化为编译期可确定的值,任何可能导致编译期无法确定值的表达式都是非法的。constexpr 函数则稍微宽松一些,如果函数返回值不要求在编译期中被使用,那么可以使用非 const 的参数输入,相当于普通的函数,但是函数内部的表达式除了输入的参数以外不允许包含其他编译期不能确定值的变量以及运算。在 C++11 中 constexpr 所修饰的函数只能包含返回语句,在 C++14 中 constexpr 所修饰的函数则可以包含更多的语句。
const int fn1(const int i) {
return i + 1;
}
constexpr int fn2(int i) {
return i + 1;
}
constexpr int fn3(const int &i) {
return i + 1;
}
constexpr int fac(int n) {
return n == 1 ? 1 : n * fac(n - 1);
}
int main() {
int x[fn1(5)];
int y[fn2(5)];
constexpr int a;
int i = 2;
constexpr int j = i + 1;
int k = fn2(i);
}
5. range-based for
range-based for 主要用于遍历一个数组或具有 begin() 和 end() 方法的容器,可以避免显式的迭代器使用。
vector<int> x(10);
int cnt = 0;
for(auto it = x.begin(); it < x.end(); it++)
{
*it = cnt++;
}
cnt = 0;
for (auto &xi : x)
{
xi = cnt++;
}
for( auto xi : x )
{
cout << xi << " ";
}
cout << endl;
6. lambda 表达式
lambda 表达式通常用来定义一些比较简单的匿名函数,并且能够像普通变量一样在局部作用域中随用随定义,而无需在文件全局作用域单独进行定义。相比普通函数,由于其定义可放在某个作用域的代码中间,所以其可以直接访问到该作用域的局部变量,而无需通过参数列表传递。另外,像普通局部变量一样,lambda 表达式也有局部的作用域与生命期,可以像普通变量一样赋值,也可以作为返回值返回。但与普通变量不同的是,lambda 表达式的赋值有点像 共享指针,即赋值操作不会在内存中产生新的 lambda 表达式,而是增加其引用计数;当 lambda 表达式离开某个局部作用域时,会导致其引用计数减少;当引用计数为 0,即离开了所有的局部作用域时,lambda 表达式就会被自动销毁,释放其表达式内部定义的局部变量。其通常形式为:
auto fn = [capture] (paras) {statements;};
- [] 用于捕获表达式所处作用域的局部普通变量,空[]代表不使用任何局部普通变量。静态变量不需要捕获。
- [&] 默认表示表达式内所用到的所有局部普通变量都是以引用方式传递的,
- [=] 默认表示 lambda 表达式内所用到的所有局部普通变量都是以值传递的。
- [&, =] 的用法错误,两者不能同时存在。
- 可以显式写明某个变量的传递方式,但必须与默认方式相反,否则错误。默认方式符号要写在最前面。
- [&, a] 变量 a 以值传递,其他的以引用方式传递,
- [=, &a] 变量 a 以引用传递,其他的以值传递。
- [&, &a] [=, a] [a, &a] 这些都是错误的写法。
- 当表达式在类成员函数里面时,需要捕获 this 指针(即 [this])才能访问类成员函数或者变量。
- (paras) 包含了可输入参数的声明,没有参数时也可省略括号,但调用时还是需要括号。
- 虽然
auto 不能作为函数输入参数类型,但在 C++14 中 lambda 表达式可以使用 auto 作为参数类型。
int main()
{
int x[5] = { 10, 3, 1, 9, 5 };
int idx[5] = { 0, 1, 2, 3, 4 };
auto cmp = [&x](int i1, int i2) { return x[i1] > x[i2]; };
sort(idx, idx + 5, cmp);
for (auto i : idx)
{
cout << i << ' ';
}
cout << endl;
return 0;
}
因为 lambda 表达式可以在局部作用域定义,并使用局部作用域的变量,所以经常被用来编写函数闭包。例如在 Python 中,我们经常需要用到生成器来持续地生成列表元素,通常通过 next() 方法来获取,这种方法可以减少提前生成整个列表所需的内存。利用 lambda 表达式,我们同样可以用 C++ 写出类似的生成器。下面以斐波那契数列生成器为例:
void Del(int *p)
{
if (NULL != p)
{
cout << "del " << p << endl;
delete p;
}
}
auto yield_fib_iter()
{
shared_ptr<int> a(new int(0), Del);
shared_ptr<int> b(new int(1), Del);
auto next = [a, b]()
{
int c = *b;
*b = *a + *b;
*a = c;
return c;
};
return next;
}
void test()
{
auto fib = yield_fib_iter();
for (int i = 0; i < 5; i++)
{
cout << fib() << endl;
}
}
int main()
{
test();
auto fib = yield_fib_iter();
for (int i = 0; i < 5; i++)
{
cout << fib() << endl;
}
return 0;
}
注意,函数闭包并没有改变变量的生命期,当函数结束时,所有函数内部的非静态变量都会被销毁(包括按值传递的参数)。如果 lambda 表达式通过引用来捕获变量,则必须保证该变量在 lambda 表达式被调用时还处在其生命期内。
在以上斐波那契数列生成器的实现中,我们使用了共享指针 shared_ptr 来存储第 n-2 和 n-1个数,当我们调用 yield_fib_iter() 函数并返回 lambda 表达式之后,yield_fib_iter() 函数内部的局部普通变量都会被销毁,包括两个共享指针本身,但是由于 lambda 表达式通过值传递的方式捕获复制了两个共享指针,共享指针所管理的动态内存的引用计数就会加 1,也就是说只要该 lambda 表达式没有被销毁,共享指针的内存引用计数就不会为 0,该内存就能一直被 lambda 表达式有效访问。当 lambda 表达式的生命期结束被销毁时,其内部的局部普通变量也会被销毁,两个共享指针的内存引用计数都变为 0,我们最初分配的动态内存就能被自动释放,而不会造成内存泄漏的问题。当我们再次调用 yield_fib_iter() 来获取另一个生成器时,注意此时返回的 lambda 表达式与之前返回的 lambda 表达式是独立的,相当于两个不同的局部变量,因此它们彼此之间不会相互影响,每个生成器都是从斐波那契数列的第一个数开始生成的。
|