详解C++中地左值、右值和移动
转载一篇我自己写在CSDN上的一篇文章
左值持久,右值短暂
C++ primer中提到过:当一个对象被用作右值时用的是对象的值(内容),当对象被用做左值时用的是对象的身份(在内存中的位置)。
int a = 10; //a是左值,10是右值 //编译出的汇编代码 movl $10, -4(%rbp)//-4(%rbp)是栈上的偏移,可以理解为a的地址,10是一个立即数
通过以上可以理解,左值是内存,右值是值了。同时也就能理解:左值持久是指直到变量销毁前都一直存在;右值短暂是指值10是只存在于CPU中的某个瞬间,当这个时间过去后,值10便消失不见;只有当一个右值被存进一个左值当中时,这个右值才能持续存在。
两种右值
一种右值就是上边提到的立即数,另一种是临时变量。其实立即数就是内置类型的临时变量。
struct TypeA{ TypeA(); }; 10; //右值 TypeA(); //右值,离开该行后将不存在
左右值引用
左值引用非常常见,是一个对象的别名。
//左值引用 int lvalue; int &lref = lvalue; //汇编结果 .type lvalue, @object .size lvalue, 4 lvalue: .zero 4 .type lref, @object .size lref, 8 lref: .quad lvalue //const 指针 int lvalue; int * const lref = &lvalue; //lref不能指向别处 //汇编结果 .type lvalue, @object .size lvalue, 4 lvalue: .zero 4 .type _ZL4lref, @object .size _ZL4lref, 8 _ZL4lref: .quad lvalue
从左值引用和const指针的汇编结果可以看出,左值引用就是const指针。在64位系统中左值引用和const指针都占用8Byte。我想c++在c语言之上提出了左值引用的概念是为了,简化指针的操作。
右值引用是一个值的别名。当创建一个右值引用变量时,其本质是将一个右值存储了起来,延续了这个右值的生命周期,再将const指针绑定到该存储空间上,并且该存储空间没有变量名能直接访问。
//用变量存储右值 int lvalue = 10; //汇编结果 .type lvalue, @object .size lvalue, 4 lvalue: .long 10 //右值引用绑定右值 int &&rref = 10; //汇编结果 .type _ZGR4rref_, @object .size _ZGR4rref_, 4 _ZGR4rref_: .long 10 .type rref, @object .size rref, 8 rref: .quad _ZGR4rref_
移动构造和移动赋值函数的调用时机
#include <iostream> #include <utility> class TypeA { public: TypeA() = default; TypeA(const TypeA&) = default; TypeA& operator=(const TypeA&) = default; TypeA(TypeA&&) = default; TypeA& operator=(TypeA&&) = default; TypeA& operator+(const TypeA&) = default; }; int main(){ TypeA a1; //调用无参构造,创建出 a1_ a1 = TypeA(); //TypeA()是个右值,调用移动赋值 TypeA a2(TypeA{12}); //1调用无参构造,构造出临时变量 //2调用移动构造 a3 = std::move(a1); //调用std::move将左值转换成右值 //调用移动赋值函数 TypeA a4(std::move(a1); //调用std::move将左值转换成右值 //调用移动构造函数 TypeA a5(a); //调用拷贝构造 a5 = a1; //调用拷贝赋值 return 0; }
这里的调用关系本质上是函数重载时,的优先匹配问题。如果向构造函数和赋值函数中传入右值,最佳匹配的就是移动构造和移动赋值;相应的如果传入的是左值,则最佳匹配将是拷贝构造和拷贝赋值。std::move
将在后文详述。
右值与右值引用
由于右值引用只能绑定到临时对象,我们得知
- 所有引用的对象将要被销毁
- 该对象没有其他用户(ps:应为是我们延续了该右值的生命周期)
这两个特性意味着:使用右值引用的代码可以自由地接管所应用对象的资源。
演示资源的转移
#include <iostream> #include <cstdlib> class TypeA { public: int *Buffer; const static size_t bufSize; public: TypeA() : Buffer(nullptr) { //获取资源 Buffer = (int*)malloc(bufSize*sizeof(int)); //初始化资源 for(int i=0; i<bufSize; ++i) Buffer[i] = i; std::cout << "Buffer: " << Buffer << std::endl; } ~TypeA(){ //释放资源 free(Buffer); Buffer = nullptr; } TypeA(const TypeA& a) : Buffer(nullptr){ //获取资源 Buffer = (int*)malloc(bufSize*sizeof(int)); //拷贝资源值 for(int i=0; i<bufSize; ++i) Buffer[i] = a.Buffer[i]; } TypeA& operator=(const TypeA& a){ //释放旧资源 free(Buffer); //获取新资源 Buffer = (int*)malloc(bufSize*sizeof(int)); //拷贝资源值 for(int i=0; i<bufSize; ++i) Buffer[i] = a.Buffer[i]; return *this; } TypeA(TypeA&& a) : Buffer(nullptr){ //盗取a的资源,因为右值a将会被销毁 Buffer = a.Buffer; //将右值a的资源做为无效 a.Buffer = nullptr; } TypeA& operator=(TypeA&& a){ //释放旧资源 free(Buffer); //盗取a的资源,因为右值a将会被销毁 Buffer = a.Buffer; //将右值a的资源做为无效 a.Buffer = nullptr; return *this; } void printBufferAddr(){ std::cout << "Buffer: " << Buffer << std::endl; } }; const size_t TypeA::bufSize = 1*1024*1024; //1 MB int main() { //三份资源 TypeA a1; TypeA a2(a1); TypeA a3 = a1; std::cout << "a1\t"; a1.printBufferAddr(); std::cout << "a2\t"; a2.printBufferAddr(); std::cout << "a3\t"; a3.printBufferAddr(); //窃取临时变量,std::move()处理后的变量的资源 TypeA arf1 = TypeA(); std::cout << "arf1\t"; arf1.printBufferAddr(); TypeA arf2 = std::move(a1); std::cout << "arf2\t"; arf2.printBufferAddr(); std::cout << "a1\t"; a1.printBufferAddr(); } //执行结果 //通过深拷贝,获取了三份不同的资源 Buffer: 0x7f81bdeb9010 //a1的资源地址 a1 Buffer: 0x7f81bdeb9010 a2 Buffer: 0x7f81bdab8010 a3 Buffer: 0x7f81bd6b7010 //通过移动构造和移动赋值,可窃取临时变量或std::move()处理后的资源 Buffer: 0x7f81bd2b6010 //临时变量的地址 arf1 Buffer: 0x7f81bd2b6010 arf2 Buffer: 0x7f81bdeb9010 a1 Buffer: 0
std::move
std::move是对于左值的一种承诺,承诺这个左值在之后将会被销毁,或者会重新初始化。这样依赖编译器就可以将左值当作右值处理,在调用构造和赋值函数的重载函数簇时,就顺理成章地匹配移动构造和移动赋值版本。(需要注意的是:std::move返回的是右值,所有不能被绑定到拷贝构造和拷贝赋值上,因而如果一个类没有定义移动构造和移动赋值,景观可以正确地调用std::move(),但不能将std::move的结果出入构造和赋值函数,也就失去了意义。)
std::move源码
namespace std { template<typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); } }
左值可以向右值转换:
int a=3, b=2; //这里隐式地将a,b转换成了右值 int c = a+b; //这里显式地将a,b转换成了右值 int c = static_cast<int>(a) +static_cast<int>(b);
引用折叠:
X& &,X& &&,X&& &,折叠成X&
X&& && 折叠成 X&&
//向move中传递左值 int a; move(a); //模板被示例化成 int&& move(int&&); T -> int; typename remove_reference<int>::type -> int; typename remove_reference<int>::type&& -> int&&; //向move中传递右值 move(10); //模板被实例化成 int&& move(int& &&); int& && 折叠成了 int&; T -> int&; typename remove_reference<int&>::type -> int; typename remove_reference<T>::type&& -> int&&;
所以,无论向std::move中传入左值还是右值,std::move返回地都是该类型地右值
#C++11新特性#