文章

C++ 引用和指针

C++ 引用和指针

引用与指针

引用指针
必须初始化可以不初始化
不能为空可以为空
不能更换目标可以更换目标
  1. 引用必须初始化,而指针可以不初始化

    1
    2
    
     int &r;		// 不合法,没有初始化引用
     int *p;		// 合法,但p是野指针
    
  2. 引用不能为空,而指针可以为空

    由于引用不能为空,所以我们在使用引用的时候不需要测试其合法性

    在使用指针的时候需要先判断指针是否为空指针,否则会引起程序崩溃

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     void test_p(int *p) {
         if(p != null_ptr)	// 对p所指对象赋值时需要先判断p是否为空指针
             *p = 3;
         return;
     }
    
     void test_r(int &r) {
         r = 3;	// 由于引用不能为空,所以无需判断r是否有效
         return;
     }
    
  3. 引用不能更换目标,只能指向初始化时指向的对象,指针可以随时改变方向

    1
    2
    3
    4
    5
    6
    7
    8
    
     int a = 1;
     int b = 2;
    
     int &r = a;		// 初始化引用r,并且指向变量a
     int *p = &a;	// 初始化指针p,并且指向变量a
    
     p = &b;			// 指针p指向了变量b
     r = b;			// 引用r依然指向了a,但是a的值变成了b
    

引用

  • 左值: 非临时的,可以出现在等号的左边或者右边,可以获取它的地址和对他赋值,分为非常量左值和常量左值
  • 右值: 临时的,只能出现在等号的右边,不可以对他取地址,分为非常量右值和常量右值

在赋值符=左侧的表达式就是左值;右侧的表达式可能是左值也可能是右值

左值引用

左值引用就是对左值的引用,给左值取别名,主要作用是避免对象拷贝

1
2
3
4
5
6
7
8
int a = 3;
int *p = &a;
const int b = 2;

int & ra = a;		// 定义了一个整型的左值引用ra,引用a变量
int *& rp = p;		// 定义了一个指向整型的指针的左值引用ra,引用了指针变量p
int & r = *p;		// 定义了一个整型的左值引用r,引用了p所指向的对象
const int & rb = b;	// 定义了一个对常量整型b的左值引用rb

右值引用

右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,给右值取别名,主要作用是延长对象的生命周期

1
2
3
int && rr1 = 10;
double && rr2 = x + y;
double && rr3 = fmin(x, y);

右值引用本质上,就是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值

1
2
3
4
5
6
7
int && ref_a = 5;
ref_a = 6;

// 等效于
int temp = 5;
int && ref_b = std::move(temp);
ref_b = 6;

右值引用引用右值,会使右值被存储到特定的位置,即右值引用变量其实是左值,可以对他取地址和赋值,那么左值引用可以引用右值引用

  • 左值引用可以指向右值,但需要const修饰,不能修改这个值
  • 右值引用可以指向左值,需要用std::move(v)
  • 声明出来的左值引用或右值引用都是左值
  • 作为函数返回值的&&是右值

对比

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      void f(const int& n) {
          n += 1; // 编译失败,const左值引用不能修改指向变量
      }
    
      void f2(int && n) {
          n += 1; // ok
      }
    
      int main() {
          f(5);
          f2(5);
      }
    
  • 左值引用只能引用左值,添加const修饰的左值引用既可以引用左值也可以引用右值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      // 1. 左值引用只能引用左值
      int t = 8;
      int & rt1 = t;
      int & rt2 = 8;	// 编译错误,因为8是左值,不能直接引用左值
    
      // 2. const左值可以引用左值
      const int & rt3 = t;
    
      // 3. const左值也可以引用右值
      const int & rt4 = 8;
      const double & r1 = x + y;
      const double & r2 = fmin(x, y);
    
  • 右值引用只能引用右值不能引用左值,但是右值引用可以引用被std::move的左值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      // 1. 右值引用只能引用右值
      int && rr1 = 10;
      double && rr2 = x + y;
      const double && rr3 = x + y;
    
      int t = 10;
      int && rrt = t;	// 编译报错,不能引用左值
    
      // 2. 右值引用可以引用被std::move的左值
      int && rrt = std::move(t);
      int *&& rr4 = std::move(p);
      int && rr5 = std::move(*p);
      const int && rr6 = std::move(b);
    

使用

左值引用场景

  • 左值引用做参数

    当引用作为参数时,效果跟利用指针作为参数的效果相当。当调用函数时,函数中的形参会被当成实参变量或对象的一个别名使用,也就是说函数中对形参的各种操作实际上就是对实参本身的操作

    优点在于参数的传递中避免了对象拷贝,提高效率

  • 左值引用做返回值

右值引用

  • 移动语义

    • 移动构造函数MyClass(Type && a): 当构造函数参数是一个右值时,优先使用移动构造函数而不是拷贝构造函数MyClass(const Type & a)
    • 移动赋值运算符Type& operator = (Type && a): 当赋值的是一个右值时,优先使用移动赋值而不是拷贝赋值运算符Type & operator = (const Type & a)

    将一个对象中的资源移动到另一个对象,主要解决减少不必要的临时对象的创建、拷贝与销毁

    在没有右值引用时,一个简单的数组类通常需要实现构造函数,拷贝构造函数,赋值运算符重载,析构函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
      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_;
      };
    

    这样程序会显得很复杂,而使用右值引用可以解决这个问题。

    STL很多容器里,比如说std::vectorpush_backemplace_back,都实现了以右值引用为参数移动构造函数移动复制重载函数

    通过右值引用,可以将上面的程序优化为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
      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_;
      };
    
    
      int main(){
          Array a;
    	 
          // 做一些操作
          // .....
    	     
          // 左值a,用std::move转化为右值
          Array b(std::move(a));
      }
    
  • std::move 将左值显式转为右值,以便调用移动构造或移动赋值

    1
    2
    
      MyVector v1(1000);
      MyVector v2 = std::move(v1); // 强制调用移动构造
    
  • vector::push_back使用std::move提高性能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      // std::vector和std::string的实际例子
      int main() {
          std::string str1 = "aacasxs";
          std::vector<std::string> vec;
    	     
          vec.push_back(str1); // 传统方法,copy
          vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
          vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
          vec.emplace_back("axcsddcas"); // 当然可以直接接右值
      }
    	 
      // std::vector方法定义
      void push_back (const value_type& val);
      void push_back (value_type&& val);
    	 
      void emplace_back (Args&&... args);
    
  • 完美转发std::forward

    在模板编程中,右值引用 + std::forward 可以保证参数转发时不丢失值类别

    std::move相比,std::forward更强大,

    std::forward<T>(u)有两个参数:Tu。 a. 当T为左值引用类型时,u将被转换为T类型的左值; b. 否则u将被转换为T类型右值。

    例1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      void B(int&& ref_r) {
          ref_r = 1;
      }
    	 
      // A、B的入参是右值引用
      // 有名字的右值引用是左值,因此ref_r是左值
      void A(int&& ref_r) {
          B(ref_r);  // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
    	     
          B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
          B(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
      }
    	 
      int main() {
          int a = 5;
          A(std::move(a));
      }
    

    例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
      void change2(int&& ref_r) {
          ref_r = 1;
      }
    	 
      void change3(int& ref_l) {
          ref_l = 1;
      }
    	 
      // change的入参是右值引用
      // 有名字的右值引用是 左值,因此ref_r是左值
      void change(int&& ref_r) {
          change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
    	     
          change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
          change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
    	     
          change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
          change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
          // 可见,forward可以把值转换为左值或者右值
      }
    	 
      int main() {
          int a = 5;
          change(std::move(a));
      }
    

引用折叠

折叠规则

组合情况声明类型折叠类型说明
引用的引用& &&左值引用 + 左值引用 = 左值引用
右值引用的引用&& &&左值引用 + 右值引用 = 左值引用
引用的右值引用& &&&右值引用 + 左值引用 = 左值引用
右值引用的右值引用&& &&&&右值引用 + 右值引用 = 右值引用
  • 模板参数推导
1
2
template<typename T>
void func(T&& arg);

上面的代码中,T&&并不总是右值引用,他是万能引用/转发引用

在类型推导时,T根据传入不同的值推导为不同的形式

  1. 传入右值

    1
    
     func(10);
    
    • 10是右值
    • T推导为int
    • 参数类型int&&
  2. 传入左值

    1
    2
    
     int x = 5;
     func(x);
    
    • x是左值
    • T推导为int&
    • 参数类型int& && –> int&
  • typedef中的折叠引用
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
struct Test {
    typedef T&& RRef;
    typedef T& LRef;
};

// 实例化时发生折叠
Test<int&>::RRef r1;   // int& && → int&
Test<int&>::LRef r2;   // int& & → int&
Test<int>::RRef r3;    // int&&
Test<int>::LRef r4;    // int&
  • auto推导
1
2
3
4
5
6
7
int x = 10;
auto&& a1 = x;    // x是左值,推导为int& &&  → int&
auto&& a2 = 10;   // 10是右值,推导为int&&

// 等价于
int& a1 = x;
int&& a2 = 10;
本文由作者按照 CC BY 4.0 进行授权