第十三章、拷贝控制

一、拷贝控制操作

1、拷贝构造函数:一个构造函数的一个参数是自身类类型的引用,且额外参数都有默认值

    class Foo{
    public:
        Foo(const Foo&);        //拷贝构造函数;必须引用类型;最好是const类型;不应该是explicit的
    }
  • 拷贝构造函数通常会被隐式使用,所以不应该是explicit
  • 如果我们没有为一个类定义拷贝构造函数,编译器会定义一个合成的拷贝构造函数
    • 合成的拷贝构造函数会将其非static成员简单的拷贝到正在创建的对象中
  • 拷贝初始化发生的情况
    • 用=定义变量时
    • 将一个对象作为实参传递给一个非引用类型的参数
      • 拷贝构造函数用来初始化非引用类类型参数,所以拷贝构造函数定义必须是引用类型
    • 从一个返回类型为非引用类型的函数返回一个对象
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

2、拷贝赋值运算符

  • 重载运算符本质上是函数,由operator关键字后接表示要定义的运算符的符号组成。
  • 运算符函数也要有一个返回类型和一个参数列表
  • 赋值运算符通常返回一个指向其左侧运算对象的引用
      Foo& operator = (const Foo&);
  • 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员

3、移动构造函数

4、移动赋值运算符

5、析构函数:释放对象使用的资源,并销毁对象的非static数据成员

    ~Foo();
  • 没有返回值,不接受参数
  • 按成员初始化顺序的逆序销毁
  • 调用析构函数的情况:
    • 变量离开作用域时被销毁
    • 一个对象被销毁时,其成员被销毁
    • 容器被销毁时,其元素被销毁
    • 临时变量,当创建它的完整表达式结束时被销毁
  • 当指向一个对象的引用或指针离开作用域时,析构函数不会执行
  • 析构函数函数体本身不直接销毁成员;在之后的隐含的析构阶段中被销毁

6、三/五法则

  • 当我们决定一个类是否需要定义自己版本的拷贝控制成员时,基本原则是首先确定这个类是否需要一个析构函数
    • 合成析构函数不会delete一个指针数据成员;这个时候需要自己定义一个析构函数来释放构造函数分配的内存
    • 如果一个类需要自己定义的析构函数,则也需要自己定义的拷贝赋值运算符和拷贝构造函数
  • 需要拷贝构造函数也需要拷贝赋值运算;反之也是;但是不一定需要自己的析构函数

7、在函数的参数列表后加上=delete指出该函数为删除的

  • 析构函数不能是删除的:如果是删除的,则无法销毁此类型的对象了
  • 一个类有const成员,则它不能使用合成的拷贝赋值运算符:因为不能被赋值

二、拷贝控制和资源管理

1、通常管理类外资源的类必须定义拷贝控制成员;这种类需要通过析构函数来释放对象所分配的资源

2、当编写一个复制运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后销毁左侧运算对象的现有成员就是安全的:防止自己赋值给自己

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps);
    delete ps;
    ps = newp;
    i = rhs.i;
    return *this;
}

三、对象移动

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

2、右值引用:必须绑定到右值的引用;通过&&来获得右值引用

  • 右值引用只能绑定到一个将要被销毁的对象
  • 可以绑定到要求转换的表达式、字面常量或是返回右值的表达式,但不能直接绑定到一个左值上
    • 返回非引用类型的函数、连同算术、关系、位以及后置递增/减运算符都可以生成右值。可以把一个const的左值引用或一个右值引用绑定到这类表达式上
    • 返回左值的引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是左值的表达式,可以把一个左值引用绑定到这类表达式上
  • 左值有持久的状态,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
    • 右值所引用的对象将要被销毁
    • 该对象没有其他用户
  • 变量是左值,因此不能将一个右值引用直接绑定到一个变量上,即使这个变量时右值引用类型也不行
  • int &&rr3 = std::move(rr1);
    • 调用move的标准库函数来获得一个绑定到左值上的右值引用
    • 调用move就意味着除了对rr1赋值或销毁它外,将不能再使用它
    • 使用move的代码应该使用std::move而不是move,这样可以避免潜在的名字冲突

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

    StrVec::StrVec(StrVec &&s) noexcept     //第一个参数为类类型的引用;其他参数都必须有默认参数
        :elements(s.elements), first_free(s.first_free)    //使用noexcept来提示编译器不抛出异常
    {
        s.elements = s.first_free = nullptr;    //确保移后源对象主语销毁无害的状态
    }
  • 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值函数了;
  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符
  • 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则这些成员默认被定义为删除的
  • 当一个类没有移动构造函数时,函数匹配规则保证该类型的对象会被拷贝,即使我们试图调用move来移动它们也是如此

4、右值引用和成员函数

  • 区分移动和拷贝的重载函数通常一个版本接收const T&;另一个版本接收&&;其他的const T&&和 T&不需要
  • void push_back(const &X);
    void push_back(&&X);

5、在参数列表后面可以放置一个引用限定符

    • 对于&限定的函数,只能将它用作左值;对于&&限定的函数,只能用作右值
    • 一个函数可以同时使用const和引用限定,但是引用限定符必须跟在const限定符后面
    • 编译器会根据对象的左/右值属性来确定使用哪个版本的函数
    • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
posted @ 2015-10-14 22:17  dylqt  阅读(225)  评论(0编辑  收藏  举报