详解C++中地左值、右值和移动

转载一篇我自己写在CSDN上的一篇文章

左值持久,右值短暂

C++ primer中提到过:当一个对象被用作右值时用的是对象的值(内容),当对象被用做左值时用的是对象的身份(在内存中的位置)^{[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将在后文详述。

右值与右值引用

由于右值引用只能绑定到临时对象,我们得知^{[primer]}

  • 所有引用的对象将要被销毁
  • 该对象没有其他用户(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新特性#
全部评论

相关推荐

霁华Tel:秋招结束了,好累。我自编了一篇对话,语言别人看不懂,我觉得有某种力量在控制我的身体,我明明觉得有些东西就在眼前,但身边的人却说啥也没有,有神秘人通过电视,手机等在暗暗的给我发信号,我有时候会突然觉得身体的某一部分不属于我了。面对不同的人或场合,我表现出不一样的自己,以至于都不知道自己到底是什么样子的人。我觉得我已经做的很好,不需要其他人的建议和批评,我有些时候难以控制的兴奋,但是呼吸都让人开心。
点赞 评论 收藏
分享
7 8 评论
分享
牛客网
牛客企业服务