一、右值引用

1.左值和右值

C++中的值要么是左值要么是右值。
一个左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西。通常来说,右值是暂时和短命的,而左值则活的很久,因为他们以变量的形式(variable)存在。我们可以将左值看作为容器而将右值看做容器中的事物。

两者区别:

①左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
②右值:不能对表达式取地址,如匿名对象。一般指表达式结束就不再存在的临时对象(即将销毁的对象)。

int i=2; //i为左值 2则为右值
int& fun1();//函数返回为左值
int fun2();//函数返回为右值
int a[2]; //a为左值常量 不可修改
class().fun(); //匿名对象为右值

2.右值引用

C++11提出右值引用的原因:为了支持移动操作。
右值引用就是必须绑定到右值的的引用。
我们通常用&&获得右值引用

左值引用与右值引用的区别:

  • 左值引用只能绑定左值,右值引用只能绑定右值(将要销毁的对象)。但是,常量左值引用可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
int i=1;
int &r=i;//正确 左值引用绑定到左值
int &&rr=i;//错误 不能将右值引用绑定到左值
int &r2=i*1;//错误 i*1是个右值
const int &r3=i*1;  //正确 const引用能绑定到右值
int && rr2=i*1; //正确 
int &&rr3=rr2;//错误 rr2为左值

注意:这里rr2的类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。

作用:

左值持久,右值短暂
故我们可以使用右值引用自由接管所引用的对象资源。

二、移动语义

1. 标准库 move 函数

虽然不能将一个右值引用绑定到一个左值上,但我们可以显示的将一个左值转换为对应的右值引用类型。
我们可以通过调用一个名为move的新标准库函数来获取绑定到左值的右值引用。函数在头文件 utility中。

int &&rr3=std::move(rr1);

move 调用告诉编译器:我们有一个左值,但我们希望他像一个右值一样处理它。我们必须认识到,调用move意味着承诺:除了对rr1赋值或销毁它以外,我们将不再使用它。在调用move之后,我们将不能对源对象的值做任何假设。
我们可以销毁一个移动后的源对象,也可以赋予它新值,但不能使用一个移动后的源对象的值。

2. 移动构造与移动赋值运算符

在某些情况下,我们没有必要复制对象(拷贝构造)——只需要移动(移动构造)它们。

C++11引入移动语义:
~源对象资源的控制权全部交给目标对象

区别于拷贝构造与拷贝赋值:

2.1 参数不同

通过函数重载机制来确定应该调用拷贝语意还是移动语意(参数是左值引用就调用拷贝语意;参数是右值引用就调用移动语意)。

  • 拷贝构造函数 Foo(const Foo&) 、拷贝赋值操作符 Foo& operator=(const Foo&) 。
  • 移动构造函数 Foo(Foo&&) 、移动赋值操作符 Foo& operator=(Foo&&) 。
2.2 移动构造函数比拷贝构造函数节省空间

拷贝构造函数完成的是整个对象或变量的拷贝,移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。移动构造必须保证移动后源对象处于一个状态——销毁它是无害的。一旦移动完成,源对象必须不再指向被移动的资源——这些资源的所有权已经归新创建的对象。

代码实现

  • 拷贝构造函数
    #include<iostream>
    #include<vector>
    #include<string.h>
    class A {
    public:
      A(){
          std::cout << "A construct..." << std::endl;
          ptr_ = new int(100);
      }
    
      A(const A & a){
          std::cout << "A copy construct ..." << std::endl;
          ptr_ = new int();
          memcpy(ptr_, a.ptr_, sizeof(int));
      }
    
      ~A(){
          std::cout << "A deconstruct ..." << std::endl;
          if (ptr_){
              delete ptr_;
          }
      }
    
      A& operator=(const A & a) {
          std::cout << " A operator= ...." << std::endl;
          return *this;
      }
    
      int * getVal(){
          return ptr_;
      }
    private:
      int *ptr_;
    };
    int main(int argc, char *argv[]){
      std::vector<A> vec;
      vec.push_back(A()); //调用拷贝构造函数
    }
    //clang++ -g -o testmove test_move.cpp

A construct... //main中创建的A对象
A copy construct ... //vector内部创建的A对象
A deconstruct ... //vector内部创建的A对象被析构
A deconstruct ... //main中创建的A对象析构

  • 移动构造函数
class A {
    public:
        ...

        A(A && a) noexcept
        {
            std::cout << "A move construct ..." << std::endl;
            ptr_ = a.ptr_;
            a.ptr_ = nullptr;
        }
        ...
};

int main(int argc, char *argv[]){
    std::vector<A> vec;
    vec.push_back(std::move(A()));  //将A转为右值引用,调用移动构造函数
}

A construct... //main中创建A对象
A move construct ... //vector内部通过移动构造函数创建A对象,减少了对堆空间的频繁操作
A deconstruct ... //释放vector中的A对象
A deconstruct ... //释放main中创建的A对象

  • 结果对比
    从上面的结果我们可以看出我们新增加的移动构造函数确实被调用了,这样就大大减了频繁对堆空间的分配/释放操作,从而提高了程序的执行效率。这里需要注意的是,在移动构造函数操作之后原A对象的指针地址已经指向NULL了,因此此时就不能再通过其访问之前的堆空间了。

noexcept关键字
对于永远不会抛出异常的函数,可以声明为noexcept的。这一方面有助于程序员推断程序逻辑,另一方面编译器可以更好地优化代码。

  • 拷贝构造函数通常伴随着内存分配操作,因此很可能会抛出异常;
  • 移动构造函数一般是移动内存的所有权,所以一般不会抛出异常。

移动构造函数通常为noexcept

提示:由于编译器优化,有些编译器有可能优化导致运行结果不一致。