1. 前言
在C++98过后,更新的最重大,最有意义的就是C++11了, C++11新增了很多实用的内容, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全, 不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多, 不仅如此,面试时也会问C++11的内容,所以我们要作为一个重点去学习
关于C++11的小故事:
本章重点:
本篇文章着重讲解C++11中新增的
统一的列表初始化{},及其底层容器:
initializer_list.并且会着重讲解C++11
中的右值引用相关内容,关于右值引用
的内容多并且杂,请同学们耐心学习!
2. 统一的列表初始化
请注意,用列表初始化和使用初始化
列表是两个完全不一样的概念!
不知道各位在写代码有没有这样写过:
vector<int> vv{1,2,3,4,5,6}; vector<int> vv = {1,2,3,4,5,6};
这就是使用列表来初始化容器!
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
即使都是用列表初始化,但种类可能不同:
class Date { public: Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) {} private: int _year; int _month; int _day; }; int main() { Date d1(2022, 1, 1); // old style // C++11支持的列表初始化,这里会调用构造函数初始化 Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 }; vector<int> v{1,2,3}; return 0; }
上面代码中,用列表初始化Date类和
vector类是不一样的,因为使用列表初
初始化Date时列表中的参数个数和类型
必须和Date中构造函数的参数个数类型
匹配,你不能写成Date d{2022}.但是在
vector初始化时,列表中的参数个数可以
是任意多个.
列表参数个数与构造函数一样的是隐式类型转换
你甚至可以这样用列表初始化:
vector<Date> vv{ {2023,12,2}, {2023,12,3}, {2023,12,4}}; map<string,int> mm{ {"西瓜",1}, {"苹果",2}, {"香蕉",3}};
3. initializer_list容器讲解
C++11中,大括号可以被识别为
一种类型,请看下面的代码验证:
auto it = { 1,2,3,4 };//li是initializer_list类型 cout << typeid(it).name() << endl;
到这里,我们就能理解为啥STL的容器
可以支持用列表初始化了,因为它的内
部的构造函数和operator=函数重载了
参数是initializer_list的版本,所以当外界
使用列表初始化时,内部会识别为
initializer_list类型就会去调用特定的构造!
随便看看几个容器的构造版本:
(注意要看C++11版本的)
并且initializer_list的内容不可修改
它指向的内容在常量区
4. 左值与右值引用的初步认识
首先,要先分清左值和右值的区别
左值的概念:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
// 以下的p、b、c、*p都是左值 int* p = new int(0); int b = 1; const int c = 2; // 以下几个是对上面左值的左值引用 int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p;
右值的概念:
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名
double x = 1.1, y = 2.2; // 以下几个都是常见的右值 10; x + y; fmin(x, y); // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y); // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 10 = 1; x + y = 1; fmin(x, y) = 1;
注意,并不能用一个值能不能修改来区分左右值
const修饰的左值也不能修改,右值引用是&&
总结:
- 区分左值和右值最常用的方法是看它
能不能取地址,能取地址的是左值! - 虽然右值不能取地址,但是可以对使用
右值引用后的变量取地址!
int&& r = 10; int* pr = &r;
5. 左值引用与右值引用比较
先说它们两个的结论:
- 左值引用只能引用左值,不能引用右值
但const左值引用能引用右值 - 右值引用只能引用右值,不能引用左值
但右值引用可以引用move
后的左值
代码检验:
// 左值引用只能引用左值,不能引用右值。 int a = 10; int& ra1 = a; // ra为a的别名 //int& ra2 = 10; // 编译失败,因为10是右值 // const左值引用既可引用左值,也可引用右值。 const int& ra3 = 10; const int& ra4 = a; -------------------------------------------------- // 右值引用只能右值,不能引用左值。 int&& r1 = 10; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 int a = 10; int&& r2 = a; // 右值引用可以引用move以后的左值 int&& r3 = std::move(a);
move是标准库中的一个函数,它可以将
一个变量/对象变成"将亡值",比如说现在
有一个数据的存在只是为了初始化另外
一个数据,那么如果不使用move的话,编译器
会将原先的数据给目标数据拷贝一份,并且
原先的数据即使已经没用了也会等到出了
作用域再销毁,加入我们使用move,编译器就
不会将原先的数据拷贝至目标数据,而是将
原先的数据直接给目标数据,而原先的数据清0!
6. 右值引用的使用场景以及价值
其实右值引用的价值刚刚已经谈到过了,
特别是在一些STL容器中,我们push一个
10,10是右值,此时不用拷贝直接此资源
做交换即可,或者说push了一个以后不需
要的值,也就是将亡值,此时也可以直接交换!
正因为如此,C++11的STL容器的构造
函数和赋值函数都重载了右值版本:
右值版本的构造很简单,直接swap资源即可
不需要像左值一样做拷贝,增加了效率!
//编译器识别为右值,直接调用右值引用版本的构造 string str("abcdef"); list<string> lt; //move后编译器识别为右值,push后原本的str就被清0了 lt.push_back(move(str));
库中重载的右值引用版本的构造和赋值
被称为"移动构造"和移动赋值"
,它们极大的
提高的很多场景下的效率!
并且在函数返回值问题上,右值引用也能
发挥意想不到的作用,请看下面的例子:
string to_string(int val) { string ret; //...将整数转换为字符串 return ret; } string s1 = to_string(123);
如果没有移动构造和移动赋值,这里
return ret后会先将ret拷贝给临时对象
然后这个临时对象再把数据赋值给
外面的s1对象,这里要经历两次拷贝
可以说效率极其低下,其过程图如下:
假如我们实现的移动构造,编译器会把
ret识别为将亡值,就会去调用移动构造,
并且经过编译器的优化后,这两步拷贝
构造最终会被优化为一步移动赋值!
7. 模板中的万能引用:&&
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
请看下面的代码:
void Fun(int &x){ cout << "左值引用" << endl; } void Fun(const int &x){ cout << "const 左值引用" << endl; } void Fun(int &&x){ cout << "右值引用" << endl; } void Fun(const int &&x){ cout << "const 右值引用" << endl; } template<class T> void PerfectForward(T&& t)//万能引用 { Fun(t); } int main() { PerfectForward(10);//右值 int a; PerfectForward(a);//左值 PerfectForward(std::move(a));//右值 const int b = 8; PerfectForward(b);//左值 PerfectForward(std::move(b));//右值 return 0; }
第一层per函数的参数既能接受左值
也能接受右值,但是假如你把代码复制
后测试,会发现在参数传递到第二层函数
时,它全部变成的左值,这是因为模板中的
万能引用会将右值退化成左值,所以后续
使用过程它就变成了左值!
使用forward可以保留对象的原生类型
void PerfectForward(T&& t) { Fun(std::forward<T>(t)); }
注意,如果有多层调用,那么每一层都要加forward
8. 总结以及拓展
C++11之后,类的六个默认成员函数
又增加了两个,移动构造和移动赋值,
对于这两个函数需要注意下面几个点:
文章内容已经完结,有问题欢迎私信
🔎 下期预告:lambda表达式和包装器🔍