详解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新特性#
美的集团公司福利 724人发布