右值引用
c++从出现就有着引用的语法,但是在c++11后又新增了右值引用的新特性,以往所学的引用成了左值引用。非左即右
无论是左值引用还是右值引用,都是给对象取别名
左值引用和右值引用
左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
//a,c,p都是左值 int a=0; const int c=5; int *p=&a; //ref都是对以上左值的引用 int& ref1=a; int& ref2=c; int*& ref3=p;
右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,**右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。**右值引用就是对右值的引用,给右值取别名。
int x=10;int y=20; //以下都是常见的右值 10; x+y; min(x,y)//函数返回值 //以下是右值引用 int&& ref1=10; int&& ref2=x+y; int&& ref3=min(x+y);
区分左值和右值最常见的方法就是能不能取地址
int a=10; int *p=&a;//左值 //错误的 int *p1=&10;//右值无法取地址
比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int a=10; //左值引用引用左值 int &ref1=a; //左值引用引用右值 const int& ref2=10; //右值引用引用右值 int&& ref3=10; //右值引用引用左值 int&& ref4=std::move(a)//右值引用可以引用move以后的左值,不能直接引用
右值引用的应用
右值引用一般用于自定义类型,右值又分为纯右值 (prvalue)、将亡值 (xvalue)
xvalue(eXpiring value)字面意思可理解为生命周期即将结束的值(将亡值),它是某些涉及到右值引用的表达式的值,左值也可以是将亡值,例如:调用一个返回类型为右值引用的函数的返回值就是xvalue。
prvalue(pure rvalue)字面意思可理解为纯右值,传统意义上的右值,例如临时对象和字面值常量(字符串字面值除外)等。
左值引用的短处
当我们在写一个函数解决某些需求时,返回值通常是需要返回函数栈帧里创建的临时变量,因为是临时变量,只能进行传值返回,但是当函数执行结束后,函数栈帧会自动销毁,函数栈帧里的局部变量也会销毁,如果我们要返回函数栈帧里创建的临时变量,就需要编译器的处理,在c++11之前:
- 编译器会对返回的对象做一份临时拷贝,拷贝出一个临时对象
- 将原对象销毁
- 在将这个临时变量返回,赋值或者调用拷贝构造给main函数栈帧里的对像
会进行两次拷贝构造,编译器会对其进行优化,优化成为一次拷贝构造,但是一些老旧编译器不会优化,仍是两次拷贝构造。
如果是内置类型,那么拷贝消耗不大,但如果是自定义类型或者是stl中的容器,
如:vector<vector> vector<list>
或者你定义了一个内存消耗极大的自定义类型,如果需要拷贝的话,会有极大的消耗,于是c++11中,提出了右值引用的语义。
右值引用解决问题
可以看到,在函数返回的时候,函数的局部变量可以视为一个将亡值,也就是它的资源将要被销毁,但是如果要返回某一个局部对象,那么可不可以将该局部变量的资源进行转移,而不是对它进行拷贝后在销毁。
移动构造
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
举个例子:
这是自定义的一个string 类型的部分代码,在它每次调用拷贝或者赋值的时候会打印出调用的拷贝或者赋值类型,便于我们观察和分析。
// 拷贝构造 string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 拷贝构造" << endl; string tmp(s._str); swap(tmp); } // 赋值重载 string& operator=(const string& s) { cout << "string& operator=(string s) -- 拷贝赋值" << endl; string tmp(s); swap(tmp); return *this; } //移动构造 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; }
在看一个简单的函数
string func() { string tmp("hello world!"); return tmp; } int main() { lx::string s1("hello world"); lx::string s3=s1.func(); return 0; }
可以看的,我们在func函数中返回了一个临时对象tmp,看移动拷贝是转移右值的资源的。
在函数中,tmp的地址为0X3353d0;
我们可以看的,在函数返回后,s3的地址也是0X3353d0,且tmp和s3所拥有的资源是相同的
说明调用了一次移动构造,直接转移了tmp中的资源,而不是在重新进行深拷贝。
我们来分析一下过程:
- 因为函数栈帧中,tmp对象还是一个左值,所以对其进行深拷贝,拷贝出一个临时变量。
- 也会对tmp进行销毁
- 临时变量是一个右值,所以调用一次移动构造,对s3进行构造
因为我用的是比较新的编译器,所以编译器对其进行了优化,所以最后显示的只有一次移动构造。
还有同学对tmp和s3的地址相同绝得很诧异。
为什么tmp都已经被销毁了,s3怎么就像是原封不动的对tmp进行copy了一样?
其实这也是编译器对此进行了一些特别复杂的处理,有兴趣的同学可以去深度挖掘一下。
STL的改动
c++11后,STL中所有容器都增加了移动构造和移动赋值。
c++11后,STL中所有容器插入函数中都增加了右值引用版本。
move()函数
这是一个帮助程序函数,用于强制对值进行移动语义, 直接使用返回值会导致被视为右值。
看以下代码:
int main() { string s1("hello world"); string s2(s1); string s3(move(s1)); return 0; }
对其进行调试,观察
对s2正常拷贝构造
往下走一步
可以看到,在对s3进行构造后,s1自身的资源被**“偷走”**了。
因为move对其进行了移动语义,转换为了右值属性,然后调用了移动构造,
将s1的资源转移了。所以会显示出这样一个结果。
所以对move()函数的调用需要谨慎。
结语
本次的博客就到这了。
我是Tom-猫,
如果觉得有帮助的话,记得
一键三连哦ヾ(≧▽≦*)o。
咱们下期再见。