目录
1 左值、右值及右值引用
左值与右值按字面上的解释是等号左边的值和等号右边的值,即左值可被赋值,右值能给左值赋值。虽然大部分情况下是这样,但是仅仅依靠与等号的相对位置还是很难判断一个值是左值还是右值,比如const变量无法被赋值,但是其是左值。于是区分左值还是右值,我们就看这个值能否被取地址,可以的话该值就是左值,否则就是右值。
引用的话不管是左值引用还是右值引用,都只是在给引用对象其一个别名。只不过右值引用的引用对象是一个右值。以下都是对右值的引用,右值引用使用两个&在变量前。
int&& rr1 = 8;
double&& rr2 = x + y;
double&& rr3 = min(x, y);
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
double x = 1.1, y = 3.3;
int&& rr1 = 4;
const double&& rr2 = x + y;
rr1 = 2;
rr2 = 1.1; // 会报错
2 const左值引用、move
由之前的知识可知左值引用只能引用左值,右值引用只能引用右值。但是对左/右值经过处理也可以实现右/左值引用。
const左值引用既可以左值也可以引用右值。
int z = 0;
int& x = z;
const int& y = 10;
对左值套上move就相当把该左值转换成右值,这时就可以右值引用了。
int x = 0;
int&& y = move(x);
3 左值引用的缺陷
从上面来看在有了左值引用的情况下,c++11引入右值引用似乎并没有什么用处。要明白右值引用的作用,我们得了解左值引用的缺陷以便后续右值引用是如何解决的。
左值引用做参数和返回值都可以减少拷贝,提高效率。
string& to_string(string& s)
{
...
return s;
}
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。传值返回就会导致深拷贝临时对象,导致效率降低。
string to_string(string& s)
{
...
string s2;
return s2;
}
4 移动构造与移动赋值
移动构造就是在构造函数参数中使用右值引用。在string实现中可知拷贝构造需要对传入的对象进行深拷贝。而移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 拷贝构造
string(string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string& s) -- 深拷贝" << endl;
string tmp(s.str);
swap(tmp);
}
这时我们在传值返回时,由于这个返回值即将销毁,编译器认为其是一个右值,就会调用移动构造。因而不需要调用拷贝构造进行深拷贝,由此提高了效率。
移动赋值的原理与移动构造相同。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
5 完美转发
在类模板中我们也会看到类似右值引用的&&符号,这时并不是表示右值引用,而是而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}