C++ 对象移动

104 阅读3分钟

C++ 对象移动

使用:C++11中新增了移动对象而非拷贝对象能力

目的: 对象拷贝后会立即被销毁,移动而非拷贝对象会大幅提升性能

  • IO类或unique_ptr这样的类,这些类都包含不能被共享的资源(如指针或IO缓存)

标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝

1 右值引用

必须绑定到右值的引用就是右值引用。我们通过&&而非&获取右值引用

  • 只能绑定到一个将要销毁的对象(也就是临时值),所以我们可以自由地将右值引用的资源”移动“到另一个对象中。

左值引用: 常规引用

int i = 42;
int &r = i;  // 左值引用
int &&rr = i; // 错误,不能将右值引用绑定到一个左值上
int &r2 = i * 42; // 错误: i*42是一个右值
const int &r3 = i * 42; // 正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; // 正确:将rr2绑定到乘法结果上
  • 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子
  • 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都是生成右值。我们可以使用const的左值或者一个右值引用绑定到这个表达式上

左值持久:右值短暂

右值特性:

  1. 字面常量
  2. 表达式求值过程中创建的临时对象

变量是左值

  • 变量表达式也有左值/右值属性
  • 变量表达式都是左值,我们不能将一个右值引用绑定到一个右值引用类型的变量上
int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式rr1是一个左值

标准库move函数

显示地将一个左值转换为对应的右值引用类型

int &&rr3 = std::move(rr1); // std::move后的对象不能再次使用了

我们可以销毁一个move后对象,也可以赋予它新值,但不能使用一个移后源对象的值

2 移动构造函数和移动赋值运算符

  • 类似于string类,如果我们自己的类也同时支持移动和拷贝,那么也能从中受益

定义移动构造函数和移动赋值运算符

  1. 第一个参数是该类类型的引用(右值引用)
  2. 任何额外的参数都必须有默认实参
StrVec::StrVec(StrVec&& s) noexcept // 移动操作不应抛出任何异常
  // 成员初始化器接管s中的资源
  : elements(s.elements), first_free(s.first_free), cap(s.cap)
  {
   // 令s进入这样的状态--对其运行析构函数是安全的
    s.elements = s.first_free = s.cap = nullptr;
  }

解析:与拷贝构造函数不同,移动构造函数不分配任何新内存:它接管给定的StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作。

移动操作、标准库容器和异常

  1. 除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,不定义noexcept,vector重新分配内存时候会调用拷贝而非移动

这就是为啥用noexpect

class StrVec {
public:
  StrVec(StrVec&&) noexcept; // 移动构造函数
}
StrVec::StrVec(StrVec &&s) noexcept; /* 成员初始化器 */

移动赋值运算符

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
  // 直接检测自赋值
  if (this != &rhs) {
    free();  // 释放已有资源
    elements = rhs.elements; // 从rhs接管资源
    first_free = rhs.first_free;
    cap = rhs.cap;
    // 将rhs至于可析构状态
    rhs.elements = rhs.first_free = rhs.cap = nullptr;
  }
  return *this;
}

\