因为面试被问到了,C++的新特性,但从未归纳过,故将整理c++11,c++17,c++20的常见特性,并用例子实现一遍。加油!!!
1.nullptr
C++用nullptr代替NULL,原因NULL在C++中会被定义为0或(void*)0,取决于编译器。
C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。
从而会引发重载的一些问题,例如
void foo(char *a); void foo(int a);
为了避免这块从而引入nullptr,nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
const class nullptr_t { public: template<class T> inline operator T*() const { return 0; } template<class C, class T> inline operator T C::*() const { return 0; } private: void operator&() const; } nullptr = {};
2.类型推导
C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。
auto
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。(register以前就是将变量放到寄存器中,不能直接用地址取,对register使用取地址操作会让register失效,反之默认是auto,现已废弃)
注意:auto只能推导出数据的不加引用(&)的数据类型
但是实测可以推导出const char *和char *,推导不出const char和char,说明顶层const的const会被忽略
此外,auto 还不能用于推导数组类型
auto 的推导规则
- 在不声明为引用或指针时,auto会忽略等号右边的引用类型和const、volatile限定
- 在声明为引用或者指针时,auto会保留等号右边的引用和const、volatile属性
auto i; // error: declaration of variable 't' with deduced type 'auto' requires an initializer //因此我们在使用auto时,必须对该变量进行初始化。 auto i= 0; //0为int类型,auto自动推导出int类型 auto j = 2.0; //auto 自动推导出类型为float int a = 0; auto b = a; //a 为int类型 auto &c = a; //c为a的引用 auto *d = &a; //d为a的指针 auto i = 1, b = "hello World"; //error: 'auto' deduced as 'int' in declaration of 'i' and deduced as 'const char *' in declaration of 'b' /* auto 作为成员变量的使用*/ class test_A { public: test_A() {} auto a = 0; //error: 'auto' not allowed in non-static class member static auto b = 0; //error: non-const static data member must be initialized out of line static const auto c = 0; }; /*c11 中的使用*/ auto func = [&] { cout << "xxx"; }; // 不关心lambda表达式究竟是什么类型 auto asyncfunc = std::async(std::launch::async, func);
decltype
decltype用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算
注意:decltype不会像auto一样忽略引用和const、volatile属性,decltype会保留表达式的引用和const、volatile属性
decltype 的推导规则
对于decltype(exp)有:
- exp是表达式,decltype(exp)和exp类型相同
- exp是函数调用,decltype(exp)和函数返回值类型相同
- 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
int a = 0, b = 0; decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值 decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值 d = 20; cout << "c " << c << endl; // 输出c 20
auto 与 decltype 配合
int e = 4; const int* f = &e; // f是底层const decltype(auto) j = f;//j的类型是const int* 并且指向的是e
基于范围的 for 循环
C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。
// & 启用了引用 for(auto &i : arr) { std::cout << i << std::endl; }
初始化列表
C++11 提供了统一的语法来初始化任意的对象,例如:
struct A { int a; float b; }; struct B { B(int _a, float _b): a(_a), b(_b) {} private: int a; float b; }; A a {1, 1.1}; // 统一的初始化语法 B b {2, 2.2};
默认模板参数
//这里用到了auto和decltype的结合推导
template<typename T = int, typename U = int> auto add(T x, U y) -> decltype(x+y) { return x+y; }
Lambda 表达式
Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
Lambda 表达式的基本语法如下:
[ caputrue ] ( params ) opt -> ret { body; };
1) capture是捕获列表;
2) params是参数表;(选填)
3) opt是函数选项;可以填mutable,exception,attribute(选填)
mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
exception说明lambda表达式是否抛出异常以及何种异常。
attribute用来声明属性。
4) ret是返回值类型(拖尾返回类型)。(选填)
5) body是函数体。
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
1) []不捕获任何变量。
2) [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
3) [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
int a = 0; auto f = [=] { return a; }; a+=1; cout << f() << endl; //输出0 int a = 0; auto f = [&a] { return a; }; a+=1; cout << f() <<endl; //输出1
4) [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
5) [bar]按值捕获bar变量,同时不捕获其他变量。
6) [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
class A { public: int i_ = 0; void func(int x,int y){ auto x1 = [] { return i_; }; //error,没有捕获外部变量 auto x2 = [=] { return i_ + x + y; }; //OK auto x3 = [&] { return i_ + x + y; }; //OK auto x4 = [this] { return i_; }; //OK auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y auto x6 = [this, x, y] { return i_ + x + y; }; //OK auto x7 = [this] { return i_++; }; //OK }; int a=0 , b=1; auto f1 = [] { return a; }; //error,没有捕获外部变量 auto f2 = [&] { return a++ }; //OK auto f3 = [=] { return a; }; //OK auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改 auto f5 = [a] { return a+b; }; //error,没有捕获变量b auto f6 = [a, &b] { return a + (b++); }; //OK auto f7 = [=, &b] { return a + (b++); }; //OK
注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。
lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
lambda表达式是不能被赋值的:
auto a = [] { cout << "A" << endl; }; auto b = [] { cout << "B" << endl; }; a = b; // 非法,lambda无法赋值 auto c = a; // 合法,生成一个副本
lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。
最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过lambda表达式给出条件,传递给count_if函数:
int value = 3; vector<int> v {1, 3, 5, 2, 6, 10}; int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });
再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:
vector<int> v(10); int a = 0; int b = 1; std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; }); // 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
当需要遍历容器并对每个元素进行操作时:
std::vector<int> v = { 1, 2, 3, 4, 5, 6 }; int even_count = 0; for_each(v.begin(), v.end(), [&even_count](int val){ if(!(val & 1)){ ++ even_count; } }); std::cout << "The number of even is " << even_count << std::endl;
新增容器
std::array栈上的数组
std::forward_list 单向链表(不提供size()函数)
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。
std::tuple元组,不可变的字典
右值引用和move语义
为什么要有右值引用。
1.效率性,如果一个变量不用了,我们用移动构造可以复用之前的内存,从而只需要实现指针的转移,而不是重新去申请一块内存进行赋值,可能会有阻塞(即使很小),效率慢。
2.安全性,当我们用左值可能调用类的成员函数,这会导致不可预知的行为,右值却是非常安全的,因为复制构造函数之后,我们不能再使用这个临时对象了,因为这个转移后的临时对象会在下一行之前销毁掉。
td::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。
当然,如果你在使用了mova(a)之后,还继续使用a,那无疑是搬起石头砸自己的脚,还是会导致严重的运行错误。
总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。