c++primer 第13章 拷贝控制

第三部分 类的设计者

  • 类的基本知识:类作用域、数据隐藏、构造函数
  • 类的重要特性:成员函数、隐式this指针、友元、const、static、mutable成员
  • ch13 构造函数:类类型对象初始化时做什么;控制对象拷贝、赋值、移动、销毁;右值引用、移动操作
  • ch14 运算符重载:允许内置运算符作用于类类型运算对象;函数调用运算符:调用对象(好像它们是函数一样),新标准不同类型可调用对象可以以一致的方式来使用;转换运算符,定义了类类型对象的隐式转换机制
  • ch15 继承和动态绑定、数据抽象(面向对象编程的基础);继承:关联类型定义更简单;动态绑定:编写类型无关的代码(忽略具有继承关系的类型之间的差异)
  • ch16 函数模板、类类型。模板,写出类型无关的通用类和函数,可变参数模板、模板类型别名、控制实例化的新方法

第13章 拷贝控制

  • 构造函数;对象拷贝、赋值、移动、销毁;拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符、析构函数
  • 定义一个类,显示/隐式指定对象拷贝、移动、赋值、销毁时做什么
  • 拷贝控制操作
    • 拷贝/移动构造函数,用同类型另一个对象初始化本对象时做什么
    • 拷贝/移动赋值运算符,将一个对象赋予同类型另一个对象时做什么
    • 析构函数,对象销毁时做什么
  • 类未定义所有拷贝控制成员时,编译器会自动为它定义缺失的操作

13.1 拷贝、赋值与销毁

  • 拷贝构造函数、拷贝赋值运算符、析构函数

13.1.1 拷贝构造函数

  • 拷贝构造函数:第一个参数是自身类类型引用,且任何额外参数都有默认值
  • 参数几乎为总为const引用
  • 会被隐式地使用,故不应该是explicit的
  • 合成拷贝构造函数
    • 没为类定义拷贝构造函数,编译器会定义一个
    • 某些类的合成拷贝构造函数会阻止拷贝该类类型对象
    • 一般情况,合成拷贝构造函数会将成员(非static)逐个拷贝到该类类型对象
    • 成员类型决定如何拷贝
      • 类类型 使用其拷贝构造函数
      • 内置类型 直接拷贝
      • 数组(不能直接拷贝一个数组) 逐元素拷贝数组类型成员
        • 数组元素类类型 元素的拷贝构造函数拷贝
  • 拷贝初始化
    • 直接初始化 编译器使用普通函数匹配
    • 拷贝初始化 将右侧运算对象拷贝到正在创建的对象中(可能需要类型转换)
      • 通常用拷贝构造函数来完成;有时用移动构造函数完成
    • 发生情况
      • =定义变量时
      • 将对象作为实参传递给非引用类型形参
      • 从返回类型为非引用类型函数返回一个对象
      • 花括号列表初始化数组中元素/聚合类成员
      • 某些类类型对所分配对象(元素)使用拷贝初始化
        • 初始化标准库容器
        • 调用insert/push成员
        • 而emplace成员创建元素都是直接初始化
  • 参数和返回值
    • 函数调用 非引用类型参数 拷贝初始化
    • 函数具有非引用的返回类型
    • 拷贝构造函数被用来初始化非引用类类型参数
  • 拷贝初始化的限制
    • explicit 构造函数类型转换时 拷贝初始化还是直接初始化较为重要
    • 当传递实参或从函数返回一个值时,不能隐式使用explicit构造函数,如需使用需显式使用
  • 编译器可以绕过拷贝构造函数
    • 编译器可跳过拷贝/移动构造函数,直接创建对象;但该程序点上,拷贝/构造函数必须存在且可访问

13.1.2 拷贝构造函数

  • 类控制对象如何初始化/赋值
  • 类未定义拷贝赋值运算符,编译器会为其合成一个
  • 重载赋值运算符
    • 重载运算符本质上是函数;名字 operator后接表示要定义的运算符的符号
    • 赋值运算符 operator=;有一个返回类型和列表参数
    • 参数表示运算对象
    • 某些运算符需定义成员函数,左侧对象绑定到隐式的this参数
      • 二元运算符(如赋值运算符) 将右侧运算对象作为显式参数传递
    • 拷贝赋值运算符接受一个与其所在类相同类型的参数;返回一个指向左侧运算对象的引用
    • 标准库通常要求保存在容器中的类型要具有赋值运算符,返回值为左侧运算对象的引用
  • 合成拷贝赋值运算符
    • 类未定义拷贝赋值运算符,编译器会生成合成拷贝赋值运算符
    • 某些类 禁止该类型对象的赋值
    • 通常 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员
      • 数组类型 逐个赋值数组元素
    • 返回指向左侧运算对象的引用

13.1.3 析构函数

  • 构造函数 初始化对象的非static数据成员
  • 析构函数 释放对象使用的资源,销毁对象的非static数据成员
  • 析构函数
    • 类的成员函数
    • 名字 波浪号 类型名
    • 没有返回值,也不接受参数
      • 因为不接受参数,故不能被重载
      • 对给定类,只有唯一析构函数
  • 析构函数完成什么工作
    • 构造函数 初始化部分 函数体;成员初始化先于函数体执行;按照在类中出现的顺序进行初始化
    • 析构函数 函数体 析构部分;首先执行函数体,然后再销毁成员;成员按初始化逆序销毁
    • 析构函数释放对象在生存期分配所有资源
    • 析构部分是隐式的,成员销毁时发生什么完全依赖于成员的类型
      • 销毁类类型成员 执行成员自己的析构函数
      • 内置类型没有析构函数
    • 隐式销毁一个内置指针类型成员不会delete它所指向的对象
    • 智能指针是类类型,具有析构函数,会在析构阶段被自动销毁
  • 什么时候会调用析构函数
    • 对象被销毁时,自动调用析构函数
      • 变量离开作用域
      • 对象被销毁,成员被销毁
      • 容器(标准库容器/数组)被销毁,元素被销毁
      • 动态分配的对象被销毁,指向它的指针应用delete运算符时被销毁
      • 临时对象,创建它的完整表达式结束时被销毁
    • 当指向对象的引用或指针离开作用域时,析构函数不会执行
  • 合成析构函数
    • 类未定义自己的析构函数,编译器合成析构函数
    • 某些类 析构函数被用来阻止该类型对象被销毁
    • 如非 析构函数体为空
    • 析构函数体本身并不直接销毁成员;
    • 成员是在析构函数体之后隐含的析构阶段被销毁的
    • 对象销毁过程 析构函数体是成员销毁步骤之外的另一部分

13.1.4 三/五法则

  • 控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数
    • 新标准增加:移动构造函数、移动赋值运算符
  • 需要析构函数的类也需要拷贝和赋值操作
    • 对析构函数的需求比拷贝函数/赋值运算符的需求更明显
    • 定义析构函数来释放构造函数分配的内存
      • 但若未定义拷贝函数/拷贝赋值运算符,合成的将会简单拷贝指针成员(多个对象可能指向相同的内存)
  • 需要拷贝操作的类也需要赋值操作,反之亦然
    • 某些类只需要拷贝或赋值操作,不需要析构函数
    • 例:类为每个对象分分配一个唯一序号;拷贝构造函数从给定对象拷贝其他数据成员,自定义拷贝赋值运算符来避免将序号赋予目的对象;但不需要自定义析构函数

13.1.5 使用=default

  • 将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本
  • 在类内用=default修饰成员声明时,隐式声明为内联的;若不希望为内联函数,应只对成员的类外定义使用=default
  • 只能对具有合成版本的成员函数使用=default(默认构造函数、拷贝控制成员)

13.1.6 阻止拷贝

  • 大多数类应显式/隐式定义默认构造函数/拷贝构造函数/拷贝赋值运算符
  • 某些类这些操作没有合理的意义,须采用某种机制阻止拷贝或赋值
    • 例:iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲
  • 定义删除的函数
    • 可将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝
    • 删除的函数:虽然声明了,但不能以任何方式使用它们
    • 在参数列表后加上=delete来指出希望定义为删除的(通知编辑器以及读者不希望定义这些成员)
    • =delete必须出现在函数第一次声明的时候
    • 可以对任何函数指定=delete
  • 析构函数不能是删除的函数
    • 若析构函数被删除,就无法销毁此类型对象
    • 对于删除了析构函数的类型,将不允许定义该类型变量或创建该类临时变量
      • 若类成员无法销毁则对象整体也无法销毁
      • 对删除了析构函数的类型,虽不能定义类型变量或成员,但可以动态分配类型对象,但不能释放这些对象
  • 合成的拷贝控制成员可能是删除的
    • 若类的某个成员的析构函数是删除的或不可访问的,则合成的析构函数被定义为删除的
    • 如类的某个成员的拷贝构造函数是删除/不可访问的,则类合成拷贝构造函数被定义为删除的;若类成员析构函数是删除/不可访问的,则类的拷贝构造函数也被定义为删除的
    • 如类的某个成员的拷贝赋值运算符是删除/不可访问的,或类有一个const/引用成员,则类的合成拷贝赋值运算符被定义为删除的
    • 类的某个成员的析构函数是删除或不可访问的(可能会创建无法销毁的对象);类有一个引用成员(改变的是引用指向的对象的值,而不是引用本身),没有类内初始化器;类有一个const成员(试图将一个新值赋值给const对象是不可能的),没有类内初始化器且其类型未显式定义默认构造函数;则类的默认构造函数被定义为删除的
    • (本质:类有一个数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的)
  • private拷贝控制
    • 新标准前,类通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝
    • 但是友元和成员函数仍然可以拷贝对象,为了阻止,将拷贝控制成员定义为private,但并不定义它们
    • (声明但不定义成员函数合法,但有一个例外(试图访问一个未定义的成员将导致一个链接错误);声明但不定义private拷贝构造函数,可以预先阻止任何拷贝该类对象的企图(将在编译阶段被标记为错误,成员/友元函数中的拷贝操作将会导致链接时错误);新标准后应该通过=delete来阻止拷贝)

13.2 拷贝控制和资源管理

  • 类外资源管理的类必须定义拷贝控制成员
    • 析构函数来释放对象所分配的资源;需要析构函数自然也要拷贝构造函数和拷贝赋值运算符
    • 定义类型对象拷贝语义
      • 类行为看起来像一个值(如标准库容器和string类)
        • 副本和原对象完全独立,改变副本不会对原对象有任何影响
        • 类通常直接拷贝内置类型(不包括指针)成员
      • 像一个指针(shared_ptr)
        • 共享状态,副本和源对象使用相同底层数据,改变副本也会改变源对象
      • IO类和unique_ptr不允许拷贝或赋值

132.2.1 行为像值的类

  • 定义拷贝构造函数,完成string的拷贝而不是拷贝指针

  • 定义析构函数释放string

  • 定义赋值运算符释放对象当前string,从右侧运算对象拷贝string

  • 类值拷贝赋值运算符

    • 结合了析构函数和构造函数的操作
    • 销毁左侧运算对象资源;将右侧运算对象拷贝数据
    • 安全:当异常发生时能将左侧运算对象置于一个有意义的状态
      • 本例:先拷贝右侧运算对象(拷贝底层string)auto newp=new string(*rhs.ps)(进行构造函数的工作),处理自赋值情况;完成拷贝后,释放左侧运算对象资源(与析构函数一样),更新指针指向新分配的string
  • 赋值运算符

    • 若将对象赋予它自身,赋值运算符须正常工作
    • 大多赋值运算符组合了析构函数和拷贝构造函数的工作
      • 好的模式:将右侧运算对象拷贝到一个局部临时对象
      • 拷贝完成后,销毁左侧运算对象成员(安全)
      • 将数据从临时对象拷贝到左侧运算对象成员中

13.2.2 定义行为像指针的类

  • 定义 拷贝构造函数 拷贝赋值运算符
    • 拷贝指针成员本身(而不是他所指向的string)
    • 需要自己的析构函数来释放接受string参数的构造函数分配的内存
  • 使用shared_ptr来管理类中的资源
    • 拷贝/赋值shared_ptr会拷贝/赋值shared_ptr所指向的指针
    • shared_ptr类自己记录有多少用户共享它所指向的对象
    • 当没有用户使用对象时,shared_ptr类负责释放资源
  • 直接管理资源
    • 引用计数
  • 引用计数
    • 初始化对象,创建引用计数(记录有多少对象与正在创建的对象共享状态)
    • 拷贝构造函数不分配新的计数器,指出给定对象的状态
    • 析构函数递减计数器 (若计数器为0,则析构函数释放状态)
    • 拷贝赋值运算符递增右侧运算对象计数器,递减左侧运算对象计数器
    • 将计数器保存在动态内存中,创建对象时,分配新的计数器,拷贝/赋值对象时,拷贝指向计数器的指针,副本和源对象指向相同的计数器
  • 定义一个使用引用计数的类
    • 构造函数分配新的成员和新的计数器,将计数器置为1
    • 拷贝构造函数拷贝所有数据成员,并递增计数器
  • 类指针的拷贝成员“篡改”引用计数
    • 拷贝/赋值HasPtr对象,副本和原对象都指向相同的string
    • 拷贝ps本身而不是它指向的string,并且拷贝时递增与string关联的计数器
    • 析构函数应递减引用计数(不能无条件deletes ps,若计数器为0,则析构函数释放ps和use指向的内存)
    • 拷贝赋值运算符 拷贝构造函数/析构函数 递增右侧运算对象的引用计数;递减左侧运算对象的引用计数,必要时释放使用的内存
      • 赋值运算符处理自赋值,递增rhs中i相同的计数器递减左侧运算对象

13.3 交换操作

  • swap
    • 交换两个元素
    • 定义了自己的swap算法将使用自定义版本,否则使用标准库定义的swap
    • 交换两个对象,进行一次拷贝和两次赋值 ;拷贝 v1->temp 赋值 v2->v1 temp->v2
    • 希望swap交换交换指针而不是分配string的新副本
  • 编写我们自己的swap函数
    • 将swap定义为friend以便能访问private成员
    • 声明为inline函数以优化代码
  • swap函数应该调用swap而不是std::swap
    • 每个swap调用应该是未加限定的
    • 如存在类型特定的swap版本,其匹配程度会优于std中定义的版本
    • 若不存在类型特定的版本,则会使用std中的版本
  • 在赋值运算符中使用swap
    • 拷贝并交换技术
    • 将左侧运算对象与右侧运算对象的副本进行交换
    • 将右侧运算对象以传值的方式传递给赋值运算符
    • 调用swap交换rhs和*this中的数据成员
    • 赋值运算符结束时,rhs被销毁 类的析构函数将执行 delete rhs现在指向的内存(释放左侧对象中原来的内存)
    • (自动处理了自赋值情况且天然异常安全;在改变左侧运算对象之前拷贝右侧运算对象)

13.4 拷贝控制示例

  • 邮件处理应用
    • Message 电子邮件消息
    • Folder 消息目录
      • 每个m可出现在多个f中,任意m的内容只有一个副本;m被改变时,任意f中的m都会看到改变后的内容
      • save/remove 向给定Folder添加/删除一条message
      • save 将m放到特定folder
      • 拷贝m时,副本和原对象是不同m对象,出现在相同的f中
        • 消息内容和f指针set的拷贝
        • 在每个包含m的f中添加一个指向新创建的m指针
      • 销毁m时 须从包含m的所有f中删除指向此m的指针
      • 将一个m对象赋予另一个m对象
        • 左侧m内容会被右侧m内容所替代
        • 须更新f集合,从原来包含左侧m的f中将它删除,并将它添加到包含右侧m的f集合中
      • 析构函数/拷贝赋值运算符 都须从包含一条m的所有f中删除它
      • 拷贝构造函数/拷贝赋值运算符 须将一个m添加到给定的一组f中
    • 拷贝赋值运算符
      • 通常执行拷贝构造函数和析构函数中也要做的工作
      • Folder类 addMsg;resMsg成员 在f对象中添加和删除m
  • Message类
    • 数据成员
      • std::string contents; 保存消息文本
      • std::set<Folder* > folders;保存指向本m所在Folder
      • 构造函数
        • 接受一个string 将给定string拷贝给contents
        • 并将folders(隐式)初始化为空集
        • 构造函数有一个默认参数 故也被当作默认构造函数
  • save和remove成员
    • save 将本m存放到给定f中
    • remove 删除本m 须更新本m的folders成员
  • Message类的拷贝控制成员
    • 拷贝m时,得到的副本应与原m出现在相同的f中
    • 遍历f指针的set,将每个指向原m的f添加一个新的m的指针
    • 拷贝构造函数和拷贝赋值运算符都需要做这个工作 故定义函数add_To_Folders()来完成公共操作
  • Message的析构函数
    • m被销毁时 需从指向此m的f中删除它remove_from_Folders()
  • Message的拷贝赋值运算符
    • 须执行拷贝构造函数和析构函数的工作
    • remove_from_Folders()
    • 拷贝消息内容和f指针
    • add_To_Folders()
    • 返回*this
  • Message的swap函数
    • 标准库定义了string和set的swap版本
    • 定义一个特定版本的swap可以避免对contents和folders成员进行不必要的拷贝
    • using std::swap 直接写swap时会自动调用最合适的版本
    • 管理指向被交换m的f指针,swap(m1,m2)调用后原来指向m1的f现在须指向m2
    • 两遍扫描
        1. 一次扫描将m从它们所在的f中删除
        1. 调用swap来交换数据成员
        1. 二次扫描来添加交换过的m

13.5 动态内存管理类

  • 某些类需在运行时分配可变大小内存空间
  • 通常使用标准库容器来保存它们的数据
    • 有些类需要自己进行内存分配(须定义自己的拷贝控制成员管理所分配内存)
  • StrVec类的设计
    • vector预先分配足够的内存来保存可能需要的更多元素
    • vector每个添加元素的成员函数会检查是否有空间容纳更多的元素
    • 使用allocator来获得原始内存;allocator分配的内存时未构造的
    • 添加新元素时 用allocator的construct成员在原始内存中创建对象
    • 删除元素时 使用destroy成员来销毁元素
    • StrVec三个指针成员指向元素所使用的内存
      • elements 分配的内存中的首元素
      • first_free 指向最后一个实际元素之后的位置
      • cap 指向未分配的内存末尾之后的位置
    • StrVec静态成员 static allcator alloc
      • alloc_n_copy 分配内存 拷贝一个给定范围内的元素
      • free 销毁构造的元素并释放内存
      • chk_n_alloc 保证StrVec至少容纳一个新元素的空间,若没有空间则调用reallocate来分配更多内存
      • reallocate在内存用完时未StrVec分配新内存
  • StrVec类定义
    • 默认构造函数 (隐式)默认初始化alloc将指针初始化为nullptr
    • size返回当前正在使用元素的数目
    • capacity返回可以保存的元素的数量
    • 当没有空间容纳新元素时,为strvec重新分配内存
    • begin/end返回指向首/尾后元素
  • 使用construct
    • push_back会调用chk_n_alloc确保有空间容纳新元素,若需要会调用reallocate
    • allocator成员来construct新的尾元素
      • 用allocator分配内存,内存是未构造的,需调用construct在内存中构造一个对象
      • 调用construct会递增first_free 使用前置递增使其指向下一个未构造的元素
  • alloc_n_copy成员
    • 拷贝或赋值StrVec成员时,会调用alloc_n_copy成员
      • 须分配独立的内存,从原StrVec对象拷贝元素至新对象
    • 用尾后指针减去首元素指针来计算需要多少空间
      • 在分配内存之后,须在此空间中构造给定元素副本
  • free 成员
    • destroy元素 释放StrVec分配的内存空间
      • for循环调用allocator的destroy成员,从构造尾元素到首元素逆序销毁所有元素
    • 元素被销毁,调用deallocate释放本StrVec对象分配的内存空间
      • 传递给deallcoate的指针须是之前某次allocate调用返回的指针,调用之前须检查element是否为空
  • 拷贝控制成员
    • 拷贝构造函数调用alloc_n_copy返回结果赋予数据成员
      • alloc_n_copy返回pair的指针
        • first 指向第一个构造的元素
        • second 指向最后一个构造的元素之后的位置
      • alloc_n_copy分配的空间恰好容纳给定的元素,cap也指向最后一个构造的元素之后的位置
    • 析构函数调用free
    • 拷贝赋值运算符在释放已有元素之前调用alloc_n_copy,再调用fre释放左侧对象,将右侧值(alloc_n_copy的返回值)赋值左侧,返回*this
  • 在重新分配内存的过程中移动而不是拷贝元素
    • 猜想reallcate做什么
      • 为一个更新更大的string数组分配内存
      • 在内存空间前一部分构造对象,保存现有元素
      • 销毁原内存空间中的元素,并释放这块内存
    • 将元素从旧空间拷贝到新空间,就会立即销毁原string
      • 拷贝这些string中的数据是多余的
      • 重新分配内存空间时,避免分配和释放string的额外开销,性能会好些
  • 移动构造函数和std::move
    • string的移动构造函数进行了指针的拷贝,而不是为字符分配内存空间
    • std::move 通常不为move提供using声明,而是直接使用std::move
  • realllcate成员
    • allocate成员分配新内存空间
      • 每次分配内存都会将容量加倍
      • for循环遍历每个已有元素,在新内存空间中construct一个对应元素
      • dest指向新构造string的内存,elem指向原数组中的元素
      • 使用后置递增运算将dest(elem)推进到各自数组中的下一个元素
    • construct第二个参数 使用move返回的值,string管理的内存将不会被拷贝,构造的每个string从elem指向的string那接管内存的所有权

13.6 对象移动

  • 对象拷贝后就立即被销毁了,移动而非拷贝对象会大幅提升性能
  • io类或unique_ptr类,包含不能被共享的资源,不能拷贝但可以移动
  • 旧标准 容器所保存的类须是可拷贝的;新标准 容器可保存不可拷贝的类型,只要能被移动即可
    • 标准库容器
      • string share_ptr类既支持移动也支持拷贝
      • io类和unique_ptr类可以移动但不能拷贝

13.6.1 右值引用

  • 支持移动操作

    • 右值引用 &&
      • 只能绑定到一个将要销毁的对象
      • 将右值引用资源移动到另一个对象中
  • 左值表达式 表示一个对象的身份;右值表达式 表示对象的值

  • 右值引用是某个对象的另一个名字

    • 左值引用(常规引用) 不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式
    • 右值引用 绑定特性 可以将一个右值绑定到上述类型,但不能直接绑定到一个左值上
  • 返回左值引用的函数

    • 连同赋值、下标、解引用和前置递增/递减运算符 都返回左值表达式;可以将左值引用绑定到这些表达式结果上
  • 返回非引用类型的函数

    • 算术、关系、位以及后置递增/递减运算符,都生成右值;不能将左值引用绑定到这类表达式上,但可以将一个const 左值引用或右值引用绑定到这类表达式上
  • 左值持久;右值短暂

    • 左值
      • 持久的状态
    • 右值
      • 字面常量;表达式求值过程中创建的临时对象
    • 右值引用只能绑定到临时对象
      • 所引用的对象将要被销毁
      • 该对象没有其他对象
      • (右值引用代码可以自由接管所引用的对象的资源)
  • 变量是左值

    • 变量 可看作只有运算对象而没有运算符号的表达式
    • 变量表达式都是左值 故不能将一个右值引用绑定到一个左值引用类型的变量上
  • 标准库move函数

    • 不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换位对应的右值引用类型
    • move调用告诉编译器 有一个左值,但是希望像一个右值一样处理它
      • 除了rr1赋值或销毁它之外,不再使用
      • 调用move之后,不能对移后源对象的值做任何假设
    • 可以销毁移后原对象,也可以赋予它新值,但不能使用一个移后源对象的值

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

  • 移动构造函数 移动赋值运算符
    • 第一个参数 类类型的一个引用 右值引用
    • 任何额外参数都必须有默认实参
  • 须确保移后源对象 销毁它是无害的,资源移动完成,源对象不再指向被移动的资源
  • 移动构造函数不分配任何新内存,接管给定的StrVec中的内存;将给定对象中的指针都置nullptr;最终源对象会被销毁
  • 移动操作、标准库容器和异常
    • 移动操作“窃取”资源,通常不分配任何对象
    • (通常情况下不发生异常,且标准库容器对异常发生时自身行为提供保障)指定noexcept
      • vector 的push_back的移动构造函数如果不指定noexcept的话,它就必须使用拷贝构造函数而不是移动构造函数
  • 移动赋值运算符
    • 执行与析构函数和移动构造函数相同的工作
    • 移动赋值运算符须正确处理自赋值
      • 直接检查this指针和rhs地址是否相同
        • 相同 左右侧运算对象指向相同的对象 不需要做任何事情
        • 不同 释放左侧运算对象所使用的内存,并接管给定对象的内存
        • 将移后源对象的指针置为nullptr
        • 关键 不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源
  • 移后源对象必须可析构
    • 从一个对象移动数据不会销毁此对象,但在移动操作完成后,源对象会被销毁
      • 须确保移后源对象进入一个可析构的状态(例如通过将移后源对象的指针置为nullptr)
      • 须保证移后源对象仍然是有效的(可以安全的为其赋予新值或者可以安全地使用而不依赖其当前值)
  • 合成的移动操作
    • 如果不声明自己地拷贝构造函数或拷贝赋值运算符,编译器会合成这些操作
      • 拷贝操作 被定义为逐成员拷贝/为对象赋值/删除的函数
    • 编译器不会为某些类合成移动操作
      • 特别是一个类定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数
      • 若一个类没有移动操作,通过正常函数匹配,类会使用对应拷贝操作来代替
    • 只当类没有定义自己任何版本拷贝控制成员并且类的每个非static数据成员都可以移动时,编译器才为它合成移动构造函数或移动赋值运算符
    • 移动操作永远不会隐式定义删除的函数
      • 若显式要求编译器生成=default移动操作,且编译器不能移动所有成员,则编译器将移动操作定义为删除的函数
    • 除一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝控制操作类似的原则
      • 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符类似
      • 类成员的移动构造函数/移动赋值运算符被定义为删除或是不可访问的,则类的移动构造函数被/移动赋值运算符定义为删除的
      • 类似拷贝构造函数;类的析构函数被定义为删除或不可访问的,则类的移动构造函数被定义为删除的
      • 类似拷贝赋值运算符;如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的
    • 移动操作和合成的拷贝控制成员还有最后一个相互作用关系
      • 一个类是否定义了自己的移动构造函数和/或移动赋值运算符,则类的合成的拷贝构造函数和移动赋值运算符会被定义为删除的
      • (所以定义了移动操作的类也须定义自己的拷贝操作)
  • 移动右值,拷贝左值······
    • 类既有移动构造函数,也有拷贝构造函数,则编译器使用普通函数匹配规则确定使用哪个构造函数;赋值情况类似
    • 不能隐式将一个右值绑定到一个左值,故此类赋值语句使用拷贝赋值运算符
    • 表达式返回结果为右值时,绑定到const拷贝和移动运算符都可,但拷贝赋值运算符需进行一次到const的转换,而移动赋值运算符精确匹配
  • ······但如果没有移动构造函数,右值也被拷贝
    • 类有一个拷贝构造函数但未定义移动构造函数
      • 编译器不会合成移动构造函数
      • 此类将有拷贝构造函数而没有移动构造函数
      • 函数匹配规则保证该类型对象会被拷贝,即使试图通过调用move来移动也会被拷贝
        • 因为可以将一个Foo&&转换为一个const Foo&(通过拷贝来“移动”)
      • 用拷贝构造函数代替移动构造函数几乎肯定安全(赋值类似)
        • 一般拷贝构造函数满足对应移动构造函数的要求
          • 拷贝给定对象,并将原对象置于有效状态
          • 实际上拷贝构造函数甚至都不会改变源对象的值
  • 拷贝并交换移动赋值运算符和移动操作
    • HasPtr类的赋值运算符有一个非引用参数,需进行拷贝初始化
      • 依赖实参类型,左值使用拷贝构造函数,右值使用移动构造函数
      • 单一赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能
    • 不管使用的是拷贝构造函数还是移动构造函数
      • 赋值运算符都swap两个对象的状态
      • 交换对象的指针成员
    • 更新三/五法则
      • 所有五个拷贝控制成员应看成一个整体
      • 某些类须定义拷贝构造函数、拷贝赋值运算符、析构函数才能正确工作
      • 为避免拷贝成员的额外开销定义移动构造函数和移动赋值运算符
  • Message类的移动操作
    • 移动构造函数和移动赋值运算符都需要更新Folder指针
    • 向容器添加元素要求分配内存,可能会抛出bad_alloc异常,故未将移动操作标记为noexcept
    • 移动赋值运算符须销毁左侧运算对象旧状态
  • 移动迭代器
    • 新标准 移动迭代器适配器
    • 改变给定迭代器解引用运算符的行为来适配此迭代器
      • 一般一个迭代器解引用运算符返回一个指向元素的左值
      • 移动迭代器的解引用运算符生成一个右值引用
    • 通过标准库make_move_iterator函数可将一个普通迭代器转换为一个移动迭代器
      • 函数接受一个迭代器参数,返回一个移动迭代器
      • 原迭代器的所有操作都在移动迭代器中照常工作,可以将一对移动迭代器传递给算法
      • uninitialized_copy 对输入序列中每个元素调用construct将元素“拷贝”到目的位置
        • 使用make_move_iterator解引用运算符生成的是右值引用,意味着construct将使用移动构造函数来构造元素
      • 标准库不保证哪些算法适用移动迭代器,哪些不适用
        • 移动一个对象可能销毁原对象,因此须确信算法在未一个元素赋值或将其传递给一个用户定义的函数后不再访问它,才能将移动迭代器传递给算法
  • 建议:不要随意使用移动迭代器
    • 移后源对象具有不确定的状态,对其调用std::move是危险的(须确保移后源对象没有其他用户)
    • 小心使用move可大幅度提升性能;但随意使用可能会导致莫名其妙、难以查找的错误而难以提升性能

13.6.3 右值引用和成员函数

  • 构造函数和赋值运算符之外成员函数还提供拷贝和移动版本将从中受益
    • 允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式
      • 一个版本接受cosnt左值引用
      • 一个接受非const右值引用
    • 一般不需要为函数定义一个接受const x&&或是一个普通 x&参数的版本
      • 希望从实参“窃取”数据时,通常传递一个右值引用,故实参不能是const的
      • 从一个对象进行拷贝操作不应该改变该对象,故通常不应该改变该对象(不需定义一个接受普通 x&参数的版本)
    • 区分移动和拷贝的重载函数通常是一个版本接受const T&,另一个版本接受T &&
    • 当调用push_back时,实参(左值/右值)决定了新元素是拷贝还是移动到容器中
  • 右值和左值引用成员函数
    • 为了维持向后兼容(例:向两个string的连接结果赋值s1+s2="wow"),新标准中仍然允许向右值赋值;但可以在自己的类中阻止这种用法
      • 强制左侧运算对象是一个左值
    • 指出thsi的左值/右值属性方式类似定义const成员函数;
      • 在参数列表后放置一个引用限定符
      • 引用限定符为&或&&,分别指出this可以指向一个左值/右值
      • 类似const限定符,引用限定符只能用于成员函数,且须同时出现在函数的声明和定义中
      • 对于&限定的函数,我们只能将它用于左值,对于&&限定的函数,只能用于右值(可以将一个右值作为赋值操作的右侧运算对象)
    • 一个函数可以同时用const和引用限定,此情况下引用限定符须跟随在const限定符之后
  • 重载和引用函数
    • 成员函数可根据是否有const以及引用引用限定符来区分重载版本
    • 可以综合引用限定符和const来区分成员函数的重载版本
    • 当定义cosnt 成员函数时可定义两个版本,一个有const限定,一个没有
    • 但引用限定不一样,若定义两个或以上具有相同名字和相同参数列表的成员函数时,须对所有函数都加上引用限定符,或者所有都不加(不论有没有const修饰)

小结

  • 每个类都会控制该类型对象拷贝、移动、赋值以及销毁时发生什么
  • 特殊的成员函数 拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数定义了上述操作
  • 移动构造函数和移动赋值运算符接受一个右值引用;拷贝版本接受一个(通常是const的)普通左值引用
  • 若类未声明这些造作,则编译器自动为其生成;若这些操作未定义为删除的,则会逐成员初始化、移动、赋值或销毁对象
    • 合成操作会依次处理每个非static数据成员,根据成员类型确定如何拷贝、移动、赋值以及销毁它
  • 分配内存或其他资源的类几乎总需要定义拷贝控制成员来管理分配内存
    • 若一个类需要拷贝构造函数,则肯定需要定义移动和拷贝构造函数以及移动和拷贝赋值运算符

术语表

  • 拷贝并交换 涉及赋值操作 首先拷贝右侧运算对象,然后调用swap交换副本和左侧运算对象
  • 逐成员拷贝/赋值 合成的拷贝/移动操作 依次处理每个非static成员;内置类型直接初始化或赋值,类类型通过成员相应拷贝/移动操作进行初始化或赋值
  • 引用计数 通常用于拷贝控制成员的设计;记录有多少对象共享状态;
    • 构造函数将引用计数置为1;销毁时减一;赋值和析构函数检查递减的引用计数是否为0,如果是则销毁对象

猜你喜欢

转载自blog.csdn.net/m0_68312479/article/details/131879614