C++ Primer第五版之(十)拷贝控制

拷贝构造函数

  1. 构造函数的第一个参数是自身类型的引用,其他参数都有默认值,该构造函数是拷贝构造函数。

  2. 类没有定义拷贝构造函数时,编绎器自动合成拷贝构造函数,即使定义了其他构造函数,编绎器也会合成一个拷贝构造函数。

  3. 成员的类型决定了如何拷贝:类类型的成员使用其拷贝构造函数来拷贝,内置类型的成员直接拷贝,数组类型逐元素拷贝数组元素,如果数组元素是类类型,使用元素的拷贝构造函数拷贝。

  4. 直接初始化与拷贝初始化的差异:使用()直接初始化时,要求编绎器使用普通函数匹配来选择与参数最匹配的构造函数,当使用=拷贝初始化时,要求编绎器将右侧运算对象拷贝到正在创建的对象中,可能需要进行类型转换,因此拷贝构造函数不应该是explicit。

  5. explicit构造函数必须显式使用不可隐式使用,拷贝构造函数在一些情况下会被隐式地引用,因此拷贝构造函数通常不应该是explicit的。

  6. 拷贝初始化不仅在使用=定义变量时会发生,下列情况也会发生:
    ①将对象作为实参传递给非引用类型形参
    ②从非引用返回类型的函数返回一个对象
    ③花括号列表初始化数组的元素或聚合类的成员
    ④初始化标准库容器或调用insert或push成员时,容器会对元素进行拷贝初始化,使用emplace成员创建的元素则进行直接初始化

拷贝赋值运算符

  1. 与类控制对象初始化一样,类也可以控制对象赋值,如果类未定义拷贝赋值运算符,编绎器会合成一个。

  2. 重载运算符本质上是函数,名字由operator关键字后接要定义的运算符号组成,赋值运算符必须定义为成员函数,左侧运算对象绑定到隐式的this参数,右侧运算对象作为显式参数传递。

  3. 赋值运算符应该返回指向左侧运算对象的引用,标准库要求保存在容器中的类型具有赋值运算符。

析构函数

  1. 构造函数初始化对象非static数据成员,并做一些其他工作,析构函数释放对象使用的资源,销毁对象非static数据成员。

  2. 名字由波浪号接类名构成,没有返回值,也不接受参数,不可被重载。

  3. 在构造函数中,成员初始化在函数体执行之前完成,且按照在类中出现的顺序初始化,在析构函数中,先执行函数体,然后按初始化顺序的逆序销毁成员。

  4. 析构函数没有构造函数初始化列表来控制成员销毁,析构部分是隐式的,依赖于成员的类型,销毁类类型的成员执行成员自己的析构函数,内置类型没有析构函数,隐式销毁内置指针类型不会delete它指向的对象,智能指针是类类型,具有析构函数,在析构阶段会被自动销毁。

  5. 对象销毁,自动调用析构函数:
    ①变量在离开作用域时被销毁
    ②对象被销毁时,其成员被销毁
    ③容器(标准库或数组)被销毁时,其元素被销毁
    ④动态分配的对象,对指向它的指针应用delete运算符时被销毁
    ⑤临时对象,当创建它的完整表达式结束时被销毁
    ⑥当指向对象的引用或指针离开作用域时,析板函数不会执行
    ⑦当类未定义析构函数时,编绎器会定义一个合成析构函数,合成析构函数体为空,类成员在析构函数体之后隐式的被销毁。

三/五法则

  1. 三个基本操作:拷贝构造函数、拷贝赋值运算符和析构函数,新标准下还有移动构造函数和移动赋值运算符。

  2. 自定义析构函数的类也需要自定义拷贝和赋值操作,需要拷贝操作的类也需要赋值操作,反之亦然。

使用=default

  1. 将拷贝控制成员定义为=default来显式要求编绎器生成合成版本拷贝构造函数,在类内使用=default修饰成员的声明,合成的函数隐式地声明为内联的,如果不希望合成成员是内联函数,可在类外成员定义时使用=default。

  2. 只能对具有合成版本的成员函数使用=default,默认构造函数或拷贝控制成员。

阻止拷贝

  1. iostream类阻止拷贝,避免多个对象读写相同的IO缓冲,不定义拷贝控制成员编绎器也会生成合成版本,所有有些类定义拷贝控制成员是为了阻止拷贝。

  2. 在新标准下,可将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,删除的函数虽然声明了它们,但不能以任何方式使用它们,在函数的形参列表后面加上=delete来定义删除的函数。

  3. 除了析构函数可以对任何函数指定=delete,析构函数已删除的类型,不能定义该类型的变量和释放指向该类型动态分配内存的指针。

  4. 类未定义拷贝控制成员时,编绎器会定义合成版本,但一些情况下,编绎器会将这些合成成员定义为删除的函数:
    ①类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的,
    ②类某个成员的析构函数是删除的或不可访问的(private),则类的合成拷贝构造函数、合成析构函数定义为删除的。

  5. 具有引用成员或无法默认构造的const成员的类,编绎器不会合成默认构造函数,类有const成员或引用成员同样不可使用合成的拷贝赋值运算符。

  6. 本质上,不可拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。

private拷贝控制

  1. 在新标准之前,类通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝,用户代码不能拷贝该类型的对象,友元和成员函数可以拷贝,将这些拷贝成员声明为private但不定义,可预先阻止任何拷贝该类型对象的操作,声明但不定义成员函数是合法的(有一个例外后篇介绍),但访问未定义成员会导致链接错误。

移动构造函数和std::move

  1. 移动构造函数通常将资源从给定对象移动而不是拷贝到正在创建的对象,调用move表示使用移动构造函数,漏掉move调用使用类的拷贝构造函数。

  2. 使用标准库move函数调用类的移动构造函数,直接使用std::move调用而不是使用using声明后的move。

  3. 需要构造函数参数args的地方,替换成std::move(args),调用move返回即使用类的移动构造函数。
    ①move调用将一个左值返回一个右值,将右值引用绑定到返回结果上,调用move以后,该对象只可赋值或被销毁,不可使用该左值对象的值。
    ②此移后源对象可以销毁可以赋新值,但不能使用一个移后源对象的值。

对象移动

  1. 重新分配内存的过程,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素,使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类,这些类包含不能被共享的资源(指针或IO缓冲),这些类的对象不能拷贝但可以移动。

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

右值引用

  1. 为支持移动操作,新标准引入新的引用类型,右值引用。

  2. 右值引用即必须绑定到右值的引用,使用&&来获得右值引用,还有一个重要特质,只能绑定到一个将要销毁的对象上。

  3. 左值有持久的状态,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。

  4. 右值引用只能绑定到临时对象,因此:
    ①所引用的对象将要销毁。
    ②该对象没有其他用户。

  5. 类似引用,右值引用也是对象的别名,区分常规引用(左值引用):
    ①常规引用不可绑定到要求转换的表达式、字面值常量或是返回右值的表达式上,右值引用可以。
    ②返回左值引用的函数、赋值、下标、解引用和前置递增/递减运算符,都是返回左值表达式,可以将左值引用绑定到这类表达式结果上。
    ③返回非引用类型的函数、算术、关系、位、后置递增/递减运算符都生成右值,不可将左值引用绑定到这类表达式上,但可以将一个const的左值引用或右值引用绑定到这类表达式上。
    ④变量可看作是只有运算对象而没有运算符的表达式,类似其他表达式,变量有左值/右值属性,变量表达式是左值,有生命周期,而右值表临时对象,故不可将右值引用绑定到变量上。

  6. 使用右值引用可以自由地接管所引用对象的资源。

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

  1. 类似拷贝构造函数,移动构造函数的第一个参数是该类型的右值引用,其他参数必须有默认参数。

  2. 移动赋值运算符执行析构函数和移动构造函数工作,类似拷贝赋值运算符,移动赋值运算符需正确处理自赋值,如果不抛出异常,可标记为noexcept。
    ①构造函数参数列表后指定关键字noexcept通知标准库构造函数不抛出任何异常。

  3. 如果类定义了拷贝构造函数、拷贝赋值运算符或析构函数,编绎器不会为类合成移动构造函数和移动赋值运算符,类没有移动操作,通过正常的函数匹配,类会使用对应拷贝操作来代替移动。
    ①当类没有定义自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编绎器会为类合成移动构造函数或移动赋值运算符。
    ②内置类型可以移动,定义了自己移动操作的元素成员可以移动,合成的移动操作实际上是利用各数据成员的移动操作。

  4. 完成资源移动后,源对象不再指向移动的资源,资源归属新创建的对象,源对象可销毁。
    ①移后源对象仍是有效的,可赋新值且不依赖当前值。
    ②移动操作并不会销毁移后源对象,确保移后源对象进入可析构的状态,可将移后源对象指针成员置为nullptr。

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

  1. 移动操作窃取资源,不分配资源,通常不会抛出异常,但抛出异常是允许的,使用关键字noexcept通知标准库移动构造函数不抛出异常,声明及定义都指定关键字。

  2. 为什么会需要noexcept呢,举例说明,标准库vector保存自定义类对象,在调用push_back时可能要求vector重新分配内存空间,如果vector使用拷贝构造函数构造新元素时,发生异常,vector可以释放新分配内存并返回,旧空间元素不受影响,但如果vector在重新分配过程使用移动构造函数,当移动了部分而不是全部元素时发生异常,此时旧空间中的移动源元素已经发生改变,而新空间未发生移动的元素不存在,此时vector自身不能保持不变,为了避免这种潜在问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存空间过程中,必须使用拷贝构造函数而不是移动构造函数,此关键字是显式告诉标准库元素类型的移动构造函数可以安全使用。

  3. 移动操作不会隐式定义为删除的函数,如果显式要求编绎器生成=default的移动操作,且编绎器不能移动所有成员,则编绎器会将移动操作定义为删除的函数。
    ①有类成员定义了自己的拷贝构造函数未定义移动构造函数,或有成员未定义自己的拷贝构造函数且编绎器不能合成移动构造函数,移动构造函数被定义为删除的,移动赋值运算符类似。
    ②类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,类的移动构造函数和移动赋值运算符被定义为删除的。
    ③类的析构函数定义为删除的或不可访问的,类的移动构造函数被定义为删除的。
    ④类成员是const的或是引用,类的移动构造函数被定义为删除的。

  4. 如果类定义了移动构造函数或移动赋值运算符,则类的合成拷贝构造函数或拷贝运算符定义为删除的,此时类必须定义自己的拷贝操作。

移动右值,拷贝左值

  1. 如果类既有移动构造函数,又有拷贝构造函数,编绎器使用普通函数匹配规则确定使用哪个构造函数。

  2. 如果类有拷贝构造函数未定义移动构造函数,编绎器不会合成移动构造函数,函数匹配规则保证该类型的对象被拷贝,即使使用move调用来移动时,用拷贝构造函数代替移动构造函数几乎肯定是安全的。以上同样适用于赋值运算符。

  3. 成员函数也可同时提供拷贝和移动版本,参数模式一个接受指向const的左值引用,一个接受指向非const的右值引用。
    ①const T&,绑定到任意可转换成T的对象
    ②T&&,精准匹配,只能绑定到类型T的可修改右值

返回右值和左值引用的成员函数

  1. 引用限定符&和&&分别表示返回的this是左值属性还是右值属性,参数列表后放置引用限定符,同时出现在函数声明和定义中,右值属性阻止向右值对象赋值。

  2. 同时有引用限定和const时,引用限定跟在const限定符之后。

  3. 成员函数有引用限定符,具有相同参数列表的同名函数所有版本都必须有引用限定。

  4. 成员函数是左值属性,允许向该函数赋值。

  5. 返回右值引用的成员函数,调用对象为右值,可改变对象的成员,返回左值const引用的成员函数,不可改变对象。

更新三/五法则

  1. 所有五个拷贝控制成员应该看作一个整体,一般来说,定义任何一个拷贝操作,就应该定义所有五个操作。

  2. 类拥有一个资源,拷贝对象必须拷贝此资源,拷贝资源会导致一些额外开销,在拷贝非必要的情况下,定义移动构造函数和移动赋值运算符可避免此问题。

移动迭代器

  1. 移动迭代器的解引用运算符生成右值引用。

  2. 通过使用标准库make_move_iterator函数将普通迭代器转换为移动迭代器,可以将一对移动迭代器传递给算法,特别是可以将移动迭代器传递给uninitialized_copy。

  3. 算法使用迭代器的解引用运算符实现各算法目的,若传递移动迭代器,解引用移动迭代器生成的是右值引用,使用右值引用构造元素则意味着使用类的移动构造函数来构造元素,从而达到移动对象而不是拷贝对象目的。

  4. 在移动构造函数和移动赋值运算符类实现代码之外的地方,只有当确信需要进行移动操作且移动操作是安全的,才可以使用std::move。

交换操作

  1. 与重排元素顺序算法一起使用的类,如果定义了自己的swap,算法将使用类自定义版本,否则算法使用标准库定义的swap。

  2. 需要声明std命名空间,且每个调用都是swap,而不是std::swap,如果存在类型特定的swap版本,匹配程序会优于std中定义的版本,如果不存在类型特定版本,则会使用std中的版本,using声明不会隐藏类型版本swap声明。

拷贝控制和资源管理

  1. 需要管理类外资源的类会通过析构函数来释放对象分配的资源,类定义析构函数也需要定义拷贝构造函数和拷贝赋值运算符。

  2. 定义拷贝操作须先确定类对象的拷贝语义,一是类的行为像值,二是类的行为像指针。
    ①类的行为像值时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响,反之亦然。行为像指针则是共享状态,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。
    ②标准库容器和string类的行为像一个值,shared_ptr类提供类似指针的行为,IO类型和unique_ptr不允许拷贝或赋值。

  3. 行为像值的类:
    ①构造函数和拷贝构造函数对每个对象都有一份拷贝,析构函数释放对象,赋值运算符组合析构函数和拷贝函数的工作,考虑自赋值的情况,先进行构造函数的工作拷贝右侧运算对象到局部临时对象,再释放原对象,最后将临时对象拷贝到本对象返回。

  4. 行为像指针的类:
    ①定义拷贝构造函数和拷贝运算符时,拷贝的是指针本身而不是指向的内容,析构函数在释放内存时不能单方面释放关联的内存,类展现类似指针行为最好的办法是使用shared_ptr来管理类中的资源,若需要直接管理资源可学习shared_ptr设计自己的引用计数。
    ②构造函数初始化对象时创建引用计数,此时引用计数为1。
    ③拷贝构造函数拷贝数据成员包括计数器,同时递增共享的计数器。
    ④析构函数递减计数器,计数器为0时释放资源。
    ⑤拷贝赋值运算符递增右侧运算对象计数器,递减左侧运算对象计数器,左侧运算对象计数器为0时释放资源。
    ⑥计数器保存在动态内存中,创建对象时,分配新的计数器,拷贝或赋值对象时拷贝指向计数器的指针,此时副本和原对象指向相同的计数器。

动态内存管理类

  1. 类在运行时分配可变大小内存空间,一般使用标准库容器来保存数据,例使用vector来管理元素的底层内存。

  2. vector预先分配足够的内存来保存可能需要的更多元素,每个添加元素的成员函数会检查是否有空间容纳更多的元素,如果有,成员函数在下一个可用位置构造对象,如果没有,vector会重新分配空间,获得新空间,将已有元素移动到新空间中,释放旧空间,并添加新元素。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值