5分钟搞懂C++左值引用和右值引用!

Posted Java开发进阶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5分钟搞懂C++左值引用和右值引用!相关的知识,希望对你有一定的参考价值。


Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

5分钟搞懂C++左值引用和右值引用!

扫码关注添加客服

进Java社群


作者丨写代码的牛顿

来源丨写代码的牛顿


“在学习C++的过程中会遇到很多难以理解的概念,今天我们就聊一聊C++中的左值引用和右值引用。”


在学习C++的过程中会遇到很多难以理解的概念,今天我们就聊一聊C++中的左值引用和右值引用。在C++11以前还没有左值引用和右值引用的概念,只有引用的概念。右值引用是由C++11引入的,为了方便和右值引用区分,我们把C++11以前的常规引用称为左值引用。


01


什么是左值引用和右值引用



int i = 42; //i是左值,可以对i取地址
int &r = i; //r是左值引用,绑定左值i



int &&rr = i; //错误!i是左值,不能绑定到右值引用rr
int &&rr3 = rr; //错误!!!!rr是一个右值引用类型的变量,是一个左值。



int &r2 = rr; //正确,rr是右值引用类型变量,变量都是左值。
int &r3 = r; //正确,r是左值引用类型变量,变量都是左值。


接下来我们看一下表达式,产生临时变量或字面常量的表达式都是右值,反之则是左值。我们依旧拿代码作为示例。


int &&rr_result = Add(1, 2); //不能对表达式取地址,所以表达式结果是一个右值,可以绑定到右值引用
int &&rr2 = i * 42; //不能对表达值取地址,所以表达式结果是右值,可以绑定到右值引用
int &&rr1 = 42; //不能对字面常量取地址,所以字面常量是右值,可以绑定到右值引用


Add函数产生一个临时变量,所以是右值。i * 42产生一个临时变量,是右值。42则是字面常量,所以也是右值。


int result = 0;
int *ptr = &(result = i * 12); //正确,可以对result取地址
int& r3 = (result = i * 12); //正确,表达式的结果存储在变量result中,可以对表达式取地址



const int &r2 = i * 42; //不能对表达值取地址,所以表达式结果是右值,可以绑定到const左值引用


i * 42是一个右值,但是const左值引用类型可以绑定到右值上。


02


拷贝和移动的区别


C++有拷贝构造函数和移动构造函数,拷贝赋值运算符和移动赋值运算符。移动和拷贝两者最大的区别是:拷贝会产生新的内存,而移动不会。通过拷贝获得的对象状态改变,不会影响到源对象,而通过移动获得的对象状态改变,会影响到源对象,而且被移动的源对象失去所有资源的控制权!拷贝会增加内存申请和数据复制的开销,而移动不会。
要实现移动语义,就必须要有:移动构造函数和移动赋值运算符
下面我们举个例子来学习一下拷贝和移动的区别。

class Object{
private:
    enum{
        NAME_LEN = 50,
    };

public:
    Object(): Name(new char[NAME_LEN]){}
    Object(const char *NameStr);
    ~Object();
    Object(const Object& B);    //拷贝构造函数
    Object& operator=(const Object& B);   //拷贝赋值运算符
    Object(Object&& B);    //移动构造函数
    Object& operator=(Object&& B);    //移动赋值运算符

    void Print();
    bool NameIsEmpty();

private:
    char *Name;
};


我们新建一个Object类,有char*类型Name成员变量,我们在构造函数中申请内存,并拷贝传入的字符串,在析构函数中释放内存。之所以这么做,主要是为了突出拷贝和移动的区别。
拷贝构造函数和拷贝赋值运算符源码实现:

Object::Object(const Object& B)
{
    if(this == &B){
        return;
    }
    
    Name = new char[NAME_LEN];
    memcpy(Name, B.Name, strlen(B.Name));

    std::cout << "Object copy constructor" << std::endl;
}

Object& Object::operator=(const Object& B)
{
    if(this == &B){
        return *this;
    }
    
    memset(Name, 0, NAME_LEN);
    memcpy(Name, B.Name, strlen(B.Name));

    std::cout << "Object copy assignment" << std::endl;

    return *this;
}


移动构造函数和移动赋值运算符源码的实现:


Object::Object(Object&& B)
{
    //防止自我移动
    if(this == &B){
        return;
    }
    
    Name = B.Name;
    B.Name = nullptr;

    std::cout << "Object move constructor" << std::endl;
}

Object& Object::operator=(Object&& B)
{
    //防止自我移动赋值
    if(this == &B){
        return *this;
    }

    if(!Name){
        delete[] Name;
    }

    Name = B.Name;
    B.Name = nullptr;
    std::cout << "Object move assignment" << std::endl;
    return *this;
}


实现了移动构造函数和移动赋值运算符,那么就可以使用std::move()标准库函数将左值变为右值,并调用他们。我们用代码示例来具体看一下如何使用std::move()库函数,并调用我们实现的移动构造函数和移动赋值运算符。


Object B1("Tangmeimei");
Object B2(B1); //调用拷贝构造函数
B2 = B1;

B1.Print();
B2.Print();

Object B4(std::move(B2));
if(B2.NameIsEmpty()){
    std::cout << "B2 Object name is empty" << std::endl;
}
B4.Print();

Object B3;
B3 = std::move(B1);
if(B1.NameIsEmpty()){
    std::cout << "B1 Object name is empty" << std::endl;
}
B3.Print();


03


运行结果和总结


下面我们实际运行一下,看一下效果。

5分钟搞懂C++左值引用和右值引用!


从运行结果我们可以看出,调用拷贝构造函数和拷贝赋值运算符最多会经历一次内存释放,一次内存申请和一次数据拷贝。而调用移动构造函数和移动赋值运算符最多会经历一次内存释放。因此数据的移动远比数据的拷贝性能高,且移动会使得源对象失去资源的所有权。


5分钟搞懂C++左值引用和右值引用!
  
    
    
  
程序员专栏
 扫码关注填加客服 
长按识别下方二维码进群
5分钟搞懂C++左值引用和右值引用!

5分钟搞懂C++左值引用和右值引用!

近期精彩内容推荐:  

5分钟搞懂C++左值引用和右值引用! 

5分钟搞懂C++左值引用和右值引用! 

5分钟搞懂C++左值引用和右值引用! 

5分钟搞懂C++左值引用和右值引用! 

5分钟搞懂C++左值引用和右值引用!



在看点这里好文分享给更多人↓↓

以上是关于5分钟搞懂C++左值引用和右值引用!的主要内容,如果未能解决你的问题,请参考以下文章

C++左值引用和右值引用

7. C++左值引用和右值引用

具有适用于左值和右值的引用参数的 C++ 函数

C++内功修炼干货,进大厂必须会的C++左值与右值,最适合小白看的文章!

[C++11]右值和右值引用

C++左值左值引用右值右值引用