设为首页 加入收藏

TOP

C++ primer读书笔记第13章:拷贝控制(四)
2016-09-12 19:03:12 】 浏览:1190
Tags:primer 读书 笔记 拷贝 控制
性是可以移动对象而非拷贝对象的能力。
  回想一下,当函数返回一个非引用的值时,会建立一个临时对象并对这个临时对象进行拷贝,而临时对象在拷贝后就立即被销毁了,在这时一个对象的拷贝其实是不必要的,在这种情况下,移动而非拷贝对象可能会大幅度提升性能。而使用移动而非拷贝的另一个原因是因为有些类(如IO类或unique_ptr)都包含不能被共享的资源(如IO缓存),因此这些对象不能拷贝但可以被移动。

13.6.1右值引用

  让我们先来回忆一下关于左值右值的知识。c++的表达式要不然是左值,要不然是右值。有一个简单的归纳是:当一个对象被用作右值时,用的是对象的值(内容),而当一个对象用作左值的时候,用的是对象的身份(在内存中的位置)。有一个重要原则:在需要右值的地方可以使用左值代替,但是不能把右值当做左值使用。当一个左值被当做右值使用时,实际使用的是它的内容。

  现在我们来看看什么是右值引用。c++11中,为了支持移动操作,引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们可以通过&&而不是&来获得右值引用。右值引用有一个很重要的特性——只能绑定到一个将要销毁的对象。
  对于常规的引用,我们可以称之为左值引用,我们不能将其绑定到要求转换的表达式、字面值常量或者返回右值的表达式(除非它是一个const引用)。而右值引用有这完全相反的特性:我们可以将一个右值绑定到这类表达式上,但是不可以将一个右值引用绑定到一个左值上。

    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;     //正确,将右值引用绑定到一个右值上

左值持久,右值短暂

  左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程创建的临时对象。由于右值引用只能绑定到临时对象,我们可知

所引用的对象将要被销毁 该对象没有其他用户

这两个特性意味着:使用右值引用的代码可以自由的接管所引用的对象的资源。

变量是左值

我们必须清楚认识到一点,变量都是左值,所以我们无法把一个右值引用绑定到一个变量上即使这个变量本身是一个右值引用:

int &&rr1 = 42;
int &&rr2 = rr1;    //错误,rr1是一个左值!!!

标准move函数

  我们可以显式的将一个左值转换成对应的右值引用类型,我们还可以通过标准库函数std::move来获得绑定到左值上的右值引用,此函数定义在头文件utility头文件中。

int r = 42;
int &&rr = std::move(r);

  我们必须意识到,当我们对一个对象使用move操作后,我们将不再使用它。在对一个对象调用move操作后,我们不能对移后源对象的值做任何假设。我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

13.6.2移动构造函数和移动赋值操作符

  为了让自定义类型支持移动操作,我们需要为其定义移动构造函数和移动赋值操作符。

移动构造函数

  移动构造函数的的第一个参数是该类类型的右值引用,任何额外的参数都必须要有默认值。而且为了完成资源移动,移动构造函数必须确保移后源对象处于这样一个状态——销毁它是无害的。所以,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象了。

StrVec(StrVec &&s) noexcept      //移动操作不应抛出任何异常
{
    //与拷贝构造函数不同的是,移动构造函数并不分配新的资源
    elements = s.elements;
    first_free = s.first_free;
    cap = s.cap;
    //将移后源对象的相关指针置为空,这样对其的析构是安全的
    s.elements = s.first_free = s.cap = nullptr;
}

  StrVec的析构函数在first_free上调用deallocate,如果我们忘记改变s.first_free的状态,那么销毁移后源对象后将会释放掉我们刚刚移动的内存。

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

  我们必须先认清两个事实:首先,虽然移动操作通常不抛出异常,但是抛出异常也是允许的;其次,标准库容器能对异常发生时其自身的行为提供保障,例如vector保证,如果我们push_back时抛出异常,则vector自身将不发生改变。
  现在我们假设vector在push_back的过程需要重新分配资源,所以其会把旧元素移动到新内存中,就像StrVec中那样。如果此过程中使用了移动构造函数,而移动构造函数在移动了部分元素后抛出了异常 ,那么旧空间中的元素已经被改变,而新空间中未构造的元素尚不存在,此时vector将不能保证抛出异常时保持自身不变的要求。但是如果此过程使用的是拷贝构造函数而非移动构造函数,那么即使拷贝构造函数抛出异常,旧元素的值仍未发生任何变化,vector可以满足保持自身不变的要求。所以为了避免这种潜在问题,除非vector知道元素的移动构造函数不会抛出异常,否则其在重新分配内存的时候,它将使用拷贝构造函数而非移动构造函数。所以如果我们希望vector这类的容器在重新分配内存时对自定义类型使用移动构造函数而非拷贝构造函数,那么我们必须将自定义类型的移动构造函数(以及移动赋值操作符)标记为noexcept(不会抛出异常)。
  

移动赋值运算符

  移动赋值运算符执行与析构函数和移动构造函数相同的工作,而且要注意的是其也必须正确处理自赋值的情况。

StrVec &operator=(StrVec &&rhs)
{
    //判断是否是自赋值
    if (this != &rhs)
    {
        free();
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        //使移后源对象处于可以安全销毁的状态
        rhs.cap = rhs.elements = rhs.first_free = nullptr;
    }
    return *this;
}

  我们费心检查自赋值看起来有些奇怪,毕竟移动赋值运算符需要右侧运算对象是一个右值。我们进行检查的原因是此右值可能是move调用的返回结果,关键点在于我们不能在使用右侧运算对象的资源之前就释放左侧对象的资源。

移后源对象必须可析构

  当我们编写一个移动操作后,必须要确保移后源对象进入一个可安全析构的状态,并且移动操作还必须保证移后源对象仍然是有效的。有效是指可以安全的对其赋新值或者可以安全使用而不依赖其当前值。但是用户不能对移后源对象的值做任何假设,一般在对其重新赋值之前不要使用它。

合成的移动操作

  与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符,但是其合成的条件不同:只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有非static数据成员都能够移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符

//X的成员都可以
首页 上一页 1 2 3 4 5 下一页 尾页 4/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇c++转码基础(1):各种编码类型及un.. 下一篇Effective Modern C++ 条款32 对..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目