10-右值引用和常量引用


右值引用

1. 什么是左值、右值

  • 左值__可以取地址__、位于等号左边;而右值无法取地址,位于等号右边
  • 唯一判定条件:有明确地址的变量即为左值,没有明确地址的字面量、临时值即为右值
  • example:
    • int a = 5;
    • a可以通过&取地址,位于等号左边,所以a是左值
    • 5无法通过&取地址,位于等号右边,所以5是右值

2. 什么是左值引用,右值引用

  • 引用的本质是别名,可以通过引用修改变量的值,传参时引用可以避免拷贝,实现原理与指针类似。
    • 左值引用
      • 能指向左值,不能指向右值的就是左值引用:
      int a =5;
      int &ref_a =a;  //左值引用,指向左值,编译通过
      int &ref_a =5;  //左值引用,指向右值,编译失败
      
      • 引用的变量是别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。
      • 但是,const左值引用是可以指向右值的(即[[16-右值引用和常量引用#^08b840|常量引用]])
    • 右值引用
      • 右值引用的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值。
      int &&ref_right =5;   //编译通过
      
      int a = 5;
      int &&ref_left = a; //编译不过,右值引用不可以指向左值
      
      ref_right = 6;  //右值引用的用途,可以修改右值
      

左右值引用的本质

  • 右值引用有办法指向左值吗?

    • 有办法,std::move:
    int a = 5; //a是个左值
    int &ref_left = a;  //左值引用指向左值
    
    int &&ref_right = std::move(a);
     //通过std::move将左值转化为右值,可以将右值引用指向
     cout << a;  //打印结果:5
    
    • 在上面的代码里,看上去是左值a通过std::move移动到右值ref_right中,那是不是a里面就没有值了?并不是,打印出a的值仍然是5 。

    • std::move是一个具有迷惑性的函数,若单从字面上理解往往以为它能把一个变量里内容移动到另一个变量,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。所以单从的__std::move(xxx)并不会有性能提升__

    • 同样的右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

        int &&ref = 5;
        ref = 6;
    
        等同于一下代码:
    
        int temp = 5;
        int &&ref = std::move(temp);
        ref = 6;
    
  • 左值引用、右值引用本身是左值还是右值?

    • 被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
    // 形参是个右值引用
    void change (int && right_value){
    	right_value = 8;
    }
    
    int main(){
    	int a = 5;   //a是一个左值
    	int &ref_left = a; //ref_left是一个左值引用
    	int &&ref_right = std::move(a);  //ref_right是一个右值引用
    
    	change(a);  //编译不过,a是左值,change参数要求右值
    	change(ref_left); //编译不过,左值引用ref_left本身也是个左值
    	change(ref_right); //编译不过,右值引用ref_right本身也是个左值
    
    	change(std::move(a)); //编译通过
    	change(std::move(ref_right));  //编译通过
    	change(std::move(ref_left));  //编译通过
    
    	chang5;  //当然可以直接接右值,编译通过
    
    	cout << &a << ' ';
    	cout << &ref_left << ' ';
    	cout << &ref_right;  //打印这三个左值的地址,都是一样
    }
    
    • 看完上述代码你可能有个问题,std::move会返回一个右值引用int&&,它是左值还是右值呢?从表达式int &&ref = std::move(a)来看,右值引用ref的指向必须是右值,所以move返回的int&&是个右值。所以右值引用既可以是左值,也可以是右值,如果有名则为左值,否则为右值
  • 从性能上将,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。

  • 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)

  • 作为函数形参是,右值引用更加灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。


std::move的广泛应用场景

  • 在实际开发中,右值引用和std::move被广泛用于在STL和自定义类中__实现移动语义,避免拷贝,从而提升性能__。
  • 一个简单的数组类通常实现如下,有构造函数,拷贝函数,赋值运算符重载,析构函数
class Array {
public:
    Array(int size) : size_(size) {
        data = new int[size_];
    }
     
    // 深拷贝构造
    Array(const Array& temp_array) {
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
     
    // 深拷贝赋值
    Array& operator=(const Array& temp_array) {
        delete[] data_;
 
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
 
    ~Array() {
       delete[] data_;
    }
 
public:
    int *data_;
    int size_;
};
  • 该类的拷贝函数、赋值运算符重载函数已经通过使用左值引用传参来避免了一次多余拷贝,但实际内部要实现深拷贝,无法免。这时,便有人提供一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后面就不要了,这就可以避免深拷贝,如:
class Array {
public:
   Array(int size) : size_(size) {
       data = new int[size_];
   }
    
   // 深拷贝构造
   Array(const Array& temp_array) {
       ...
   }
    
   // 深拷贝赋值
   Array& operator=(const Array& temp_array) {
       ...
   }

   // 移动构造函数,可以浅拷贝
   Array(const Array& temp_array, bool move) {
       data_ = temp_array.data_;
       size_ = temp_array.size_;
       // 为防止temp_array析构时delete data,提前置空其data_      
       temp_array.data_ = nullptr;
   }
    

   ~Array() {
       delete [] data_;
   }

public:
   int *data_;
   int size_;
};
  • 这么做有两个问题:
    • 不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
    • 无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_=nullptr;这行会编译不过。当然函数参数可以改为非const;Array(Array& temp_array,boolmove){...},这样也有问题,由于左值引用不能接右值,Array a = Array(Array(),trun);这种调用方式就没用了。
  • 于是__右值引用的出现解决了这个问题__,在STL的很多容器中,都实现了__以右值引用为参数__的移动构造函数和移动赋值重载函数,或者其他函数,或者其他函数,最常见的如std::vectorpush_backemplace_back。参数的左值引用意味着拷贝,右值引用意味着移动。
class Array {
public:
    ......
 
    // 优雅
    Array(Array&& temp_array) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
public:
    int *data_;
    int size_;
};
  • 如何使用:
// 例1:Array用法
int main(){
    Array a;
 
    // 做一些操作
    .....
     
    // 左值a,用std::move转化为右值
    Array b(std::move(a));
}

常量引用

  • 作用:
    • 常量引用主要用来修饰形参,防止误操作
    • 在函数形参列表中,可以加const修饰形参,防止形参改变实参
 		int a =10;
 		int & ref 1= a;    //合法的
 		
 		int & ref2 = 10  
 		 //× 不允许的,引用必须引一块合法的内存空间
 		 
 		const int & ref3 = 10;  
 		/*合法的,加上const后,编译器将代码修改为
 		 int temp =10; 
 		 const int & ref3 = temp; */
comments powered by Disqus