右值引用是C++11中才被提出来的新概念,而以前的版本中也有引用,但是是指的左值引用。归根结底,左右值引用都是给对象取别名。
1.区分左值和右值
提起左值和右值很多小伙伴可能第一时间会有点小蒙圈,敲了好长时间代码了,对于这个概念可能有点蒙圈,其实模糊的说可以以=号为分界线,左边的叫左值,右边的叫右值。
1.1左右值特点 :
左值:是一个表示数据的表达式,如变量名或解引用的指针等
- 可以放在等号的左右边
- 左值可以修改
- 左值可以取地址
int main() { int a = 10; int b = a; b = 10; const int c = 5; int* p = new int(0); //以上的a,b,c,*p都是左值 cout << b << endl; cout << *p << endl; return 0; }
右值:也是一个表示数据的表达式,如字母常量、表达式的返回值、函数的返回值(不能是左值引用返回)等。
- 右值不可以取地址
- 右值不可以直接修改
- 右值只能放在等号右边
- 右值往往是没有名称的
int main() { int x,y=10; //以下是常见的三种右值 x+y // 表达式返回值 func(x,y) //函数返回值 5 //常量 }
- 之所以右值无法被取地址是因为右值的本身是一个常量值或者是临时变量,这些常量值和临时变量并没有被储存起来,所以就没有他们的地址。
右值又被细分为纯右值和将亡值:
- 纯右值: 就是指等号右边的常数,上式中的5
- 将亡值:其实就是中间变量的过渡,过渡之后就消亡,可以细分两种:
- 函数的临时返回值:例如 int a = func(3); func(3)的返回值是右值,副本拷贝给a,然后消失。
- 表达式 像(x+y),其中(x+y)是右值。
1.2左值引用和右值引用
左值引用
左值引用就是对左值的引用,给左值取别名,通过“&”来声明。
int main() { int a = 10; int b = a; b = 10; const int c = 5; int* p = new int(0); //以上的a,b,c,*p都是左值 int& ra=a; const int& rc = c; int*& rp=p; int& cpp=*p; }
以上是几种常见的左值引用。
右值引用:
右值引用就是对右值的引用,给右值取别名,通过“&&”来声明。
int main() { double x = 4.1, y = 4.2; //以下几个都是常见的右值 x + y; func(x, y); 5 //以下几个都是对右值的右值引用 int&& rr1 = 5; double&& rr2 = x + y; double rr3 = func(x, y); return 0; }
- 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,可以用const修饰右值引用。
int main() { double x = 4.1, y = 4.2; int&& rr1 = 5; const double&& rr2 = x + y; //修改右值 rr1 = 10; rr2 = 20; return 0; }
左值引用能引用右值吗?或者右值引用能引用左值吗?
绝大多数情况下是不能的,因为左值是可以修改的而右值是不能修改的,所以涉及到权限放大问题,一般来说两者都不成立。但是要想左值引用来引用右值可以用const。
所以const左值引用既可以引用左值也可应引用右值。
同理,右值引用在绝大多数情况下不能引用左值,但是能引用move()中的左值。move()是C++11新增的函数,在后面我们会介绍。
左右值引用总结:
- 左值引用只能引用左值不能引用右值,但是能引用const修饰的右值。
- 右值引用只能引用右值不能引用左值,但是能引用move后的左值。
2.右值引用的提出
2.1引用的价值
说起引用的价值不得不提起:提高效率,减少拷贝。
2.2左值引用能解决哪些问题?
- 做参数:a、减少拷贝,提高效率,b、做输出型参数(这个左值引用几乎可以解决所有的问题) 。
- 做返回值: a、减少拷贝,提高效率,b、引用返回,可以修改返回对象。(这个左值引用能解决大部分问题,但是有一些还无法解决,所以就提出了右值引用。
- 如果函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板。
2.3 减少拷贝构造
我们以to_string函数为例,这个函数的返回对象是一个局部变量。此时就不能再使用引用返回了,只能用传值返回一次一次的传了。但是现在的编译器都会进行优化处理,所以一般进行一次拷贝构造而实际进行的是两次拷贝构造。
namespace cl
{
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += (x + '0');
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
int main()
{
int x = 10;
string ret = tmp::to_string(-3456);
}
之所以不能用左值引用做返回值,就是因为to_string函数最后会被析构,这里在用传值引用就会出大问题。
有的编译器进行优化,就不再产生临时变量而是直接进行一次拷贝构造。
但是并不是所有的代码都可以优化或者是有的情况必须要进行两次拷贝构造时就要用到右值引用了。
为了解决上述的问题c++11就提出了新的内容。
2.4右值引用和移动语句
左值引用是直接加&来使用的,但是右值引用不是直接加&&使用的,而是有它自己的规则。
移动构造
为了更好的解决这个问题,又给移动语句新定义了两元大将:移动拷贝和移动赋值。
- 我们以模拟实现的string来说明。
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<string> #include<assert.h> using namespace std; namespace cl { class string { public: typedef char* iterator; iterator begin() { return _str; //返回字符串中第一个字符的地址 } iterator end() { return _str + _size; //返回字符串中最后一个字符的后一个字符的地址 } //构造函数 string(const char* str = "") { _size = strlen(str); //初始时,字符串大小设置为字符串长度 _capacity = _size; //初始时,字符串容量设置为字符串长度 _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0') strcpy(_str, str); //将C字符串拷贝到已开好的空间 } //交换两个对象的数据 void swap(string& s) { //调用库里的swap ::swap(_str, s._str); //交换两个对象的C字符串 ::swap(_size, s._size); //交换两个对象的大小 ::swap(_capacity, s._capacity); //交换两个对象的容量 } //拷贝构造函数(现代写法) string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象 swap(tmp); //交换这两个对象 } //赋值运算符重载(现代写法) string& operator=(const string& s) { cout << "string& operator=(const string& s) -- 深拷贝" << endl; string tmp(s); //用s拷贝构造出对象tmp swap(tmp); //交换这两个对象 return *this; //返回左值(支持连续赋值) } //析构函数 ~string() { delete[] _str; //释放_str指向的空间 _str = nullptr; //及时置空,防止非法访问 _size = 0; //大小置0 _capacity = 0; //容量置0 } //[]运算符重载 char& operator[](size_t i) { assert(i < _size); //检测下标的合法性 return _str[i]; //返回对应字符 } //改变容量,大小不变 void reserve(size_t n) { if (n > _capacity) //当n大于对象当前容量时才需执行操作 { char* tmp = new char[n + 1]; //多开一个空间用于存放'\0' strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0') delete[] _str; //释放对象原本的空间 _str = tmp; //将新开辟的空间交给_str _capacity = n; //容量跟着改变 } } //尾插字符 void push_back(char ch) { if (_size == _capacity) //判断是否需要增容 { reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍 } _str[_size] = ch; //将字符尾插到字符串 _str[_size + 1] = '\0'; //字符串后面放上'\0' _size++; //字符串的大小加一 } //+=运算符重载 string& operator+=(char ch) { push_back(ch); //尾插字符串 return *this; //返回左值(支持连续+=) } //返回C类型的字符串 const char* c_str()const { return _str; } private: char* _str; size_t _size; size_t _capacity; }; }
移动构造:是一个右值引用而构造函数是const修饰的左值引用,而移动构造的本质就是将参数右值的资源进行剽窃从而占为己有而避免深拷贝,而这个参数右值(将亡值)也将在不久后消失,所以也可以算是物尽其用了。
- 这里我们在说一下右值到类型:
- 内置类型右值:纯右值
- 自定义类型右值:将亡值-----即将被消耗掉的值
namespace cl { class string { public: //移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造--资源转移" << endl; swap(s); } private: char* _str; size_t _size; size_t _capacity; }; }
就单单看移动构造和拷贝构造的书写格式,拷贝构造把const去掉,把&换成&&j就ok了。
对于左值来说,会调用拷贝构造,对于遇见的右值去调用移动构造,但是此时我们用move还会发生一些场景。
s1的所以东西全部都转移个s3了包括它本身的地址,这个就叫资源转移,这就叫专业。因为用到move函数时把s1当将亡值了,你都快没了,我继承你的家产,娶你的老婆没啥毛病吧,哈哈。
移动构造和拷贝构造的区别:
- 在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数。
- 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)。
- string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小。
给string类增加移动构造后,对于返回局部string对象的这类函数,在返回string对象时就会调用移动构造进行资源的移动,而不会再调用拷贝构造函数进行深拷贝了。
在上面已经说过了,编译器为了提高效率,会进行一系列的优化,例如将调用的两次深拷贝减少到一次。但是有点编译器缺不会优化。在C++11提出移动构造以后,移动构造也会从2次被优化为1次。
但如果我们不是用函数的返回值来构造一个对象,而是用一个之前已经定义出来的对象来接收函数的返回值,这时编译器就无法进行优化了。
这时当函数返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再调用赋值运算符重载函数将这个临时对象赋值给接收函数返回值的对象。
- 编译器并没有对这种情况进行优化,因此在C++11标准出来之前,对于深拷贝的类来说这里就会存在两次深拷贝,因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现。
- 但在深拷贝的类中引入C++11的移动构造后,这里仍然需要再调用一次赋值运算符重载函数进行深拷贝,因此深拷贝的类不仅需要实现移动构造,还需要实现移动赋值。
移动赋值
移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值。
//移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } //移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; }
移动赋值和原有operator=函数的区别:
- 在没有增加移动赋值之前,由于原有operator=函数采用的是const左值引用接收参数,因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
- 增加移动赋值之后,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。
- string原有的operator=函数做的是深拷贝,而移动赋值函数中只需要调用swap函数进行资源的转移,因此调用移动赋值的代价比调用原有operator=的代价小。
在实现移动赋值函数之前,该代码的运行结果理论上应该是调用一次拷贝构造,再调用一次原有的
operator=
函数,但由于原有operator=
函数实现时复用了拷贝构造函数,因此代码运行后的输出结果会多打印一次拷贝构造函数的调用,这是原有operator=
函数内部调用的。
3.完美转发
3.1万能引用&&
之所以叫万能引用就是&&代表的不再是右值引用而是既能用左值引用也能用右值引用的“万能充”。
template<typename T> void PerfectForward(T&& t)//万能引用 { //…… }
右值引用和万能引用的区别就是,右值引用需要是确定的类型,而万能引用是根据传入实参的类型进行推导,如果传入的实参是一个左值,那么这里的形参t就是左值引用,如果传入的实参是一个右值,那么这里的形参t就是右值引用。
万能引用也叫做引用折叠,如果说传进去的是个左值,那&&折叠成一个&,如果说传进去的是一个右值,那还是&&。
但是如果说原本的右值下一步变成左值后再被传参到万能引用中那他被当做左值传进去还是右值传进去还是右值传进去呢?
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<typename 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);//const左值 PerfectForward(std::move(b));//const右值 return 0; }
PerfectForward(0函数的参数类型是一个万能引用,而我们在PerfectForward函数中调用func函数就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值,能够匹配到对应版本的Func函数。
为毛全部都是左值引用啊?
实际调用PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数。
根本原因就是:右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。
也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。
3.2完美转发
完美转发的保持属性
要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数。
就比如上面的要想让右值不退化成左值,就可以用到forward函数。
template<class T> void PerfectForward(T&& t) { Func(std::forward<T>(t)); }
经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值。