统一的列表初始化
{}初始化
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include<iostream> using namespace std; int main() { int arr1[] = { 1,2,3,4,5,6 }; int arr2[]{ 1,2,3,4,5,6 }; for (auto e : arr1) { cout << e << " "; } cout << endl; for (auto e : arr2) { cout << e << " "; } cout << endl; return 0; }
#include<iostream> using namespace std; struct Point { int _x; int _y; }; int main() { Point* p1 = new Point[2]{ {1,1},{2,2} }; return 0; }
#include<iostream> using namespace std; class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } private: int _year; int _month; int _day; }; int main() { //这三个效果是相同的 Date d1(2023, 5, 25); Date d2{ 2023, 5, 25 }; Date d3 = { 2023, 5, 25 }; return 0; }
也就是说这里用花括号进行初始化调用的是类的构造。
也就是说,C++11几乎可以一切都可以用花括号初始化,包括变量(但是不建议这样)。
std::initializer_list
来看下面这段代码:
#include<iostream> #include<vector> #include<list> using namespace std; int main() { vector<int> arr{1, 2, 3, 4, 5, 6};//这里的初始化为什么可以随意改变元素数量呢? auto a = { 10,20,30 };//来看看这个花括号初始化成了什么类型 cout << typeid(a).name() << endl;//这里拿到的是类型的字符串 return 0; }
这是initializer_list类型的使用文档https://cplusplus.com/reference/initializer_list/initializer_list/
这个类似一个常量数组,有两个指针指向数组的开始和结束(其实也是迭代器)。
并且这个vector可以利用这个类型进行初始化的。
其实就相当于将initializer_list类型中的数据遍历然后push_back()到vector里面。
这种类型的实用处就是:
//这里就不用初始化一个pair类型的然后在插入map中了,因为里面是匿名对象的初始化 map<string, string> str = { {"字符串","string"},{"排序","sort"} };//里面的两个小花括号也可以理解为一个pair类型的initializer_list数组
声明
auto
这个经常用,自动推导左边对象类型。
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将
其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
decltype
关键字decltype将变量的类型声明为表达式指定的类型。
#include<iostream> using namespace std; int main() { int x = 0; cout << typeid(x).name() << endl; decltype(x) y; cout << typeid(y).name() << endl; return 0; }
那么decltype使用的地方在哪里呢?
#include<iostream> using namespace std; template<class T1, class T2> void F(T1 t1, T2 t2) { decltype(t1 * t2) ret = t1 * t2;//这里万一涉及到整型提升,不知道提升到哪个类型就可以自动推导,不至于丢失精度 cout << typeid(ret).name() << endl; cout << ret << endl; } int main() { F(1, 2); F(1, 2.2); return 0; }
nullptr
这个之前也经常用。
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
STL中一些变化
新容器
array
https://legacy.cplusplus.com/reference/array/array/
这个新容器和数组的功能没什么区别,不如vector好用,比普通数组多一个越界检查的报错。
forward_list
https://legacy.cplusplus.com/reference/forward_list/forward_list/
这是个单链表。
这里的区别就是,每个节点少一个指针的大小,并且没有头插头删(并不是那么好用)
已有容器的新接口
这里以vector举例:
这四个其实就是上面的正迭代器和反迭代器,c只是为了显示是const版本的而已,看起来更容易辨别。
这个接口是缩容的接口,如果空间浪费的实在是太大,可以用一下(用时间换空间)。
还有这两个接口,与右值引用和可变模板参数有关,下面会结合这个接口讲解。
右值引用和移动语义
左值引用和右值引用
什么是左值?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。
定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
什么是右值?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
右值引用就是对右值的引用,给右值取别名。
注意:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
#include<iostream> using namespace std; int main() { // 左值引用只能引用左值,不能引用右值。 int a = 10; int& ra1 = a;//ra为a的别名 //int& ra2 = 10;//编译失败,因为10是右值 //const左值引用既可引用左值,也可引用右值。 const int& ra3 = 10; const int& ra4 = a; int a = 10; int&& r2 = a; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 // 右值引用可以引用move以后的左值 int&& r3 = std::move(a); return 0; }
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
右值引用使用场景和意义
左值引用最大的意义就是函数传参,返回值,减少拷贝。
那么左值引用的缺点是什么?
看下面代码:
template<class T> T func(const int x) { T ret; return ret;//这里ret是局部变量,出作用域就会销毁,所以是一个传值返回 }
右值引用的价值之一就是补齐这最后一块短板。