Skip to content

右值引用

clarkehe edited this page Apr 21, 2020 · 3 revisions
  1. 关于引用
  • 引用就是“别名”,指向已有的对象。
  • 引用不是指针,却有着和指针一样功能,又优于指针:不会有空指针与野指针的问题。
  • 引用变量在声明时必须初始化,指定要引用的对象。引用与对象之间的指向关系一旦确定,不可更改。
  • 引用变量完全代表它指向的对象,使用引用变量如同使用对象本身一样。
  • 可以有常引用,代表被引用的对象是const的。对象如果是const的,只能使用常引用来引用它。
  • 常引用可以引用非const对象。常(左值)引用是“万能引用”,后面会说到。
  • 使用引用的场景有:(常)引用返回、(常)引用传递参数。目的是减少对象的copy构造。在引用返回时,要注意对象的生命周期。
  1. 左值、右值及右值引用
  • 在引入右值引用之前,我们一般只会关心左值,其实右值是一直存在的。对于一个赋值(=)表达式,我们估且认为=左边的是左值,=右边的是右值。
  • 例如:int a, b = 1, c = 2; a = b + c;,a就是左值,b + c 就是一个右值。
  • 例如:int Fun() {int i = 1; return i;} int a = Fun();, a就是左值,Fun()的返回值就是一个右值。
  • 右值的特点是:没有名字,不可取址(&)。可以大概理解为临时对象。
  • 什么是右值引用?顾名思义,就是对右值的引用。所有右值都可以被右值引用,右值引用的符号是&&,比左值引用多一个&。
  • 例如:int Fun() {int i = 1; return i;} int &&a = Fun(); a就是一个右值引用,引用的一个临时的整形变量。就本例而言, Fun()返回是一个整形变量,使不使用右值引用,没有太大区别。如果Fun()返回的是一个类对象,使用右值引用,就有意义了。
  • 例如:std::string重载了操作符+,返回的是一个新的std::string对象。对于std::string operator+(const std::string &s1, const std::string &s2)的返回值,我们有三种处理方式:值copy、const左值引用、右值引用。很显然,右值引用是最优化的。不仅减少了一次copy构造,而且可以继续操作被引用的右值。
      std::string s1 = "Test";
      //  std::string&& r1 = s1;          // error: can't bind to lvalue

      //s对右值的copy构造
      std::string s = s1 + s1;	          // okay: copy constructor called

      //对右值的左引用,只能是常引用
      const std::string& r2 = s1 + s1;    // okay: lvalue reference to const extends lifetime
      //  r2 += "Test";                   // error: can't modify through reference to const

      //对右值的右值引用
      std::string&& r3 = s1 + s1;      // okay: rvalue reference extends lifetime
      r3 += "Test";                    // okay: can modify through reference to non-const
  • 右值引用将原来没有名字、不能取址的右值引用上,使右值“可见”了。
  • 右值还可以被const左值引用。这个在编码中很常见,可能没有留意。例如:
     void Fun(const std::string &str)
     {
        std::cout << str;
     }
     std::string s1 = "Test";
     Fun(s1 + s1);
  • 关于右值引用的规则。总结下就是:右值引用只能引用右值,const右值引用只能引用const右值。
  1. 右值引用解决了什么问题?
  • 关于右值引用解决的问题,我们可先看一个例子。类HasPtrMem通过实现了Copy构造函数完成了对象的深拷贝。现在通过接口GetTem获取一个HasPtrMem的对象。
class HasPtrMem
{
protected:
    int* d;

public:
    HasPtrMem():d(new int(0))
    {}

    HasPtrMem(const HasPtrMem &h):d(new int(*h.d))
    {}

    ~HasPtrMem()
    {
        delete d;
    }
};

HasPtrMem GetTem() { return HasPtrMem(); }

int main()
{
    HasPtrMem a = GetTem();
}
  • 通过调试分析(关掉优化选项)可以发现:类HasPtrMem的构造函数调用了1次,Copy构造函数调用了2次,析构函数最终会调用3次。
  • 代码逻辑上并没有什么问题。但仔细分析会发现:GetTem()函数在返回时,会Copy构造生成一个临时对象,这个临时对象又Copy构造初始化了变量a;Copy构造函数实现了深拷贝,可能会是一个性能的热点。是否有可以优化的方法?
  • 再分析下临时对象:临时对象在Copy构造中被初始化,申请了内存,然后再去初始化a,最后临时对象申请的内存在析构中释放。临时对象似乎在做一些没有意义的事情。我们是否可以在生成临时对象的时候不申请内存,析构时也不用释放内存?答案是:可以的。跟右值引用有关。先看代码。
    HasPtrMem(HasPtrMem&& h):d(h.d)
    {
        h.d = NULL;
    }
  • 上面的代码是实现一个移动语义的构造函数,参数是一个右值引用类型。前面说过:右值引用是用于引用右值的,且右值大多都是临时对象。这里可以理解为对临时对象的引用。
  • 加入移动语义的构造函数后,再运行代码,会发现:构造函数调用了1次,移动语义的构造函数调用了2次,析构还是3次。
  • 看下移动语义的构造函数的实现:与Copy构造函数不同,它没有新分配内存,而是将h引用的临时对象内存“占为已有”,并将其指针置空。移动语义的构造函数为什么能这么做?因为h引用的是一个临时对象。临时对象不会被其他代码所引用,相当于一个不被程序员可见的对象,程序员最终关注是变量a。
  • 结论:加入了移动语义的构造函数后,原来2次的Copy构造函数的调用变成了移动语义的构造函数的调用。很显然,移动语义的构造函数更轻量,效率更高。有说人,可以用返回指针或输出参数【引用】来解决这个问题,确实可以;但使用指针有判断空等问题,使用输出参数不太方便编码(单条语句连续调用)。
  1. 移动语义
  • 上面提到的移动语义的构造函数,就是右值引用的一个重要应用。也叫右值引用的移动语义。
  • 前面已经说过什么是右值引用了。右值引用在C++11中分为两类:将亡值(expiring Value),纯右值(Pure Rvalue)。前面说的临时对象就是将亡值。纯右值一般是一些没有名字的常量,如:2、‘c’、true。
  • 移动语义则是跟将亡值(临时对象)相关的。何为“移动”?指的是对象的资源可以被移走,被其他对象使用。
  1. 转发语义
Clone this wiki locally