第13章-拷贝控制

拷贝控制

  • 拷贝控制操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。


1 拷贝、赋值和销毁

1-1 拷贝构造函数

  • 拷贝构造函数在几种情况下都会被隐式地使用,所以,拷贝构造函数通常不应该是explicit的。
  • 拷贝构造函数必被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果参数不是引用类型,那么调用永远不会成功——为了调用拷贝构造函数,必须拷贝它的实参,但是为了拷贝实参,又需要调用实参的拷贝构造函数,如此无限循环。
  • 在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。但是即使编译器跳过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的。

1-2 拷贝赋值运算

  • 如果一个运算符时成员函数,其左侧对象就绑定到隐式的this参数。
  • 赋值运算符通常应该返回一个指向其左侧运算对象的引用。

1-3 析构函数

  • 在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化的。在一个析构函数中,首先执行函数体,然后销毁成员,成员按照初始化顺序的逆序销毁。
  • 认识到析构函数自身并不直接销毁成员非常重要。成员是在析构函数体之后隐含的析构阶段中被销毁的。

1-4 三/五法则

1-5 使用=default

  • 删除的函数是这样一种函数:虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出该函数被定义为删除的。
  • 与=default不同,=delete必须出现在函数的第一次声明时候。因为,一个默认的成员只影响为这个成员而生成的代码,所以=default直到编译器生成代码时才需要;而另外一方面,编译器需要直到一个函数是删除的,以便禁止试图使用它的操作。
  • 对于删除了析构函数的类型,虽然不能定义这种类型的变量或成员,但是可以动态分配这种类型的对象,只是不能释放这些对象。
  • 合成的拷贝控制成员可能是删除的,遵循以下原则:
  • 如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数被定义为删除的;
  • 一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的;
  • 对于具有引用或无法默认构造的const成员的类,编译器不会为其合成默认构造函数;
  • 如果一个类有const成员或引用成员,合成拷贝赋值运算符被定义为删除的。
  • 在C++11新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private来组织拷贝的。但是,友元和成员函数仍旧可以拷贝对象,为了组织它们进行拷贝,再将这些拷贝控制成员声明为private的同时不定义它们。这样,试图拷贝对象的用户代码在编译阶段被标记为错误;成员函数或友元函数中的拷贝操作也将会导致链接时出错。


2 拷贝控制和资源管理

  • 可以定义拷贝操作,使得类的行为看来想一个值或者像一个指针:
  • 类的行为像一个值,意味着它应该也有自己的状态。当拷贝一个对象时,副本和原对象是完全对立的;
  • 类的行为像一个指针则共享状态。副本和原对象共享相同的底层数据。

3-1 行为像值的类

  • 当编写赋值运算符时,要记住:如果将一个对象赋予它自身,赋值运算符必须能正确工作;大多数赋值运算组合了析构函数和拷贝构造函数的工作。

3-2 定义行为像指针的类

3-3 交换操作


4 拷贝控制示例


5 动态内存管理类


6 对象移动

6-1 右值引用

  • 右值引用就是必须要绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。
  • 左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
  • 变量是一个左值,因此不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不可以。
  • 标准库中的move函数,可以获得绑定到左值上的右值引用,move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。调用move就意味着承诺:除了对源对象赋值或销毁操作外,将不再使用它。在调用move之后,不能对移后源对象的值做任何假设。

6-2 移动构造和移动赋值运算符

  • 除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。
  • 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept,以下是理由:
  • 如果重新分配过程中使用了移动构造函数,且在移动部分而不是全部元素后抛出了一个异常,就会产生问题——旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。在此情况下,源对象将不能满足自身保持不变的要求;
  • 另一方面,如果使用了拷贝构造函数且发生了异常,它可以很容器的满足要求。此情况下,当在新内存中构造元素时,旧元素保持不变,如果此时发生异常,新空间可以释放新分配(但还未成功构造的)内存并返回,源对象中原有的元素仍然存在;
  • 故为了避免这种潜在问题,除非对象知道元素类型的移动构造函数不会抛出异常,否则在分配内存的过程中,就必须使用拷贝构造函数而不是移动构造函数。如果想使用移动,就必须通过noexcept标记移动操作是可以安全使用的。
  • 在移动操作之后,移动源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
  • 合成移动操作定义为删除所遵循的原则:
  • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数,移动构造函数则被定义为删除的;
  • 如果有类成员的移动构造函数或移动赋值运算符被定义为删除或不可访问的,类的移动构造函数或移动赋值运算符则被定义为删除的;
  • 如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的;
  • 如果类成员是const或引用,则类的移动赋值运算符被定义为删除的。
  • 如果类定义了一个移动构造函数或移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符被定义为删除的。
  • 如果没有移动构造函数,右值也被拷贝构造。
  • C++11新标准库中定义了一种移动迭代器适配器。通过标准库的make_move_iterator函数将一个普通迭代器转换成一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。

6-3 右值引用和成员函数

  • 允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算相同的参数模式——一个版本接受指向const的左值引用,第二个版本接受一个指向非const的右值引用。
  • 我们指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符。
  • 引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数声明和定义中。
  • 一个函数可以同时拥有const和引用限定符。此时,引用限定符必须跟在const限定符之后。
  • 就像一个成员函数可以根据是否有const限定符来区分重载版本一样,引用限定符也可以区分重载版本。
  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本必须都具有引用限定符。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值