[读书笔记] C++Primer (第5版) 第13章 拷贝控制

本文深入探讨了拷贝构造函数、拷贝赋值运算符、移动构造函数及移动赋值运算符的概念与应用。解析了它们在C++中的工作原理,包括如何管理资源、定义拷贝控制成员以及实现移动操作。同时,介绍了右值引用的作用和标准库move函数的使用。
拷贝构造函数用同类型的另一个对象初始化本对象会做什么
拷贝赋值函数将一个对象赋予同类型的另一个对象会做什么
移动构造函数同拷贝构造
移动赋值函数同拷贝赋值
析构函数当此类型对象销毁时做什么

称这些操作为拷贝控制操作。

1.拷贝构造函数:

   Foo(const &Foo);       // 拷贝构造函数:第一个参数是自身类型的引用,且任何额外参数都有默认值

   可以是非const的,但一般总是const。

   必须是引用。在传参(非引用类型)时,如果不是引用,则会无限循环的调用拷贝构造函数。

   在几种情况下会被隐式的使用,通常不是explicit的

   合成拷贝构造函数:当没有给这个类定义拷贝构造函数时,编译器生成的一个。

   当合成拷贝构造函数不是用来阻止拷贝该类型对象(拷贝构造函数为删除 = delete)。它会将参数的成员(非static的)逐个拷贝到正在创建的对象中。每个成员的类型决定它如何拷贝,内置对象直接拷贝,数组会逐元素拷贝,类类型会使用它的拷贝构造函数。

   拷贝初始化发生的情况:

  • 使用=定义变量,例: string s2 = s1;
  • 将一个对象作为实参,传给一个非引用类型的形参
  • 一个返回类型为非引用的该类型的对象
  • 用花括号列表初始化一个数组

   对于初始化容器,使用insert或push会进行拷贝初始化,使用emplace会直接初始化。

   在拷贝初始化过程中,编译器可以跳过拷贝/移动构造函数,直接创建对象。

2.拷贝赋值运算符:

   重载运算符:参数表述运算符的运算对象。如果运算符是成员函数,左侧运算对象就绑定隐式的this参数。

   赋值运算符通常返回一个指向其左侧运算对象的引用。

   类没有靠背赋值运算符时,编译器会为其合成拷贝赋值运算符。性质类型合成拷贝构造函数。

3.析构函数:

   构造函数初始化类中的非static成员和一些其他操作。析构函数释放对象使用的资源,并销毁对象的非static成员。

   构造函数有一个初始化部分和函数体,初始化在函数体之前完成,按照他们在类中出现的顺序进行初始化。

   析构函数也有一个函数体和析构部分,首先执行函数体,然后销毁成员,按初始化顺序的逆序销毁。

   无论何时一个对象被销毁,都会自动调用其析构函数。

   当一个对象的引用或指针离开作用域,析构函数不会被执行。

   未定义自己的析构函数,编译器也会生成一个合成析构函数。

4.三/五法则:

   三个基本操作就可控制类的拷贝操作:拷贝构造函数,拷贝赋值函数和析构函数。

   如果一个类定义了拷贝操作,它就应该定义所有五个操作。

   需要析构函数的类也需要拷贝和赋值操作。

   需要拷贝操作的类也需要赋值操作,反之亦然。

5.阻止拷贝:

   只能对具有合成版本的成员函数使用 = default;

   有些类是不允许拷贝的,如流(istream等)。这时需要删除拷贝操作,定义删除的函数 ( = delete;)。

   与 = defalut不同, = delete必须出现在第一次声明处。可以对任何函数指定delete。

   析构函数是不可以指定删除的。

6.拷贝控制:

   管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,首先确定该类的拷贝语义。

   分为行为像值的类和行为像指针的类。

  • 行为像值的类:意味着类有自己的状态,副本和原对象完全独立,改变副本不会对原对象造成影响。定义拷贝构造函数:实现成员的拷贝,而不是指针拷贝。定义析构函数:释放该成员。定义赋值函数:重新拷贝,并释放该对象的旧成员。大多数赋值运算符组合了析构函数和构造函数的工作。注意要可以实现自拷贝,先把旧成员的值拷贝出来,在销毁旧成员。
  • 行为像指针的类:共享状态,副本和原对象使用同一个底层资源,改变副本也会改变原对象。拷贝成员本身,而不是它所指向的对象。类似于shared_ptr需要使用引用计数。引用计数不能单纯的作为该类的成员变量,需要存在动态内存中。拷贝操作时也进行指针一样的拷贝。计数器的变化类似shared_ptr。最后洗后函数判断计数器,为0则释放内存。

7.交换操作:

   对于管理资源的类,除了定义拷贝控制成员,还需要定义一个名为swap的交换函数。

   交换两个对象时,如果没有自定义的版本,会使用标准库定义的swap。

   交换两个对象需要完成一次拷贝(v1拷贝给临时变量)和两次赋值(v2赋值给v1,临时变量赋值给v2)。

   如果我们希望交换的元素交换指针即可完成,则使用标准库的会带来不必要的内存分配。

   交换操作不会必要的,对于分配了资源的类,这是一种很重要的优化手段。

   void swap(ClassA &l, CkassA &r)

   {

       // 交换l和r中的指针。 在swap函数应该使用swap,而不是std::swap

   } 

   赋值运算符中使用swap : 

   ClassA& ClassA::operator=(ClassA a) {swap(*this, a); return*this;}   // 传入的是对象所以函数结束后,a销毁

8.对象移动:

   新标准一个最主要的特征是可以移动而非拷贝对象的能力。

   有些类移动会提高性能,而有一些比如流和unique_ptr都包含不能共享的资源,只能移动不能拷贝。

9.右值引用:

   必须绑定到右值的引用(使用&&获得右值引用)。可以自由地将一个右值引用的资源“移动”到另一个对象中。

   左值和右值可以理解为赋值号的左面和右面。

   左值引用不能绑定到要求转换的表达式、字面常量或是返回右值的表达式(可以使用const的左值引用)。

   右值引用有着完全相反的特性:可以将右值绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。

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

   右值引用的一个重要特征---只能绑定到一个将要销毁的对象。

   由于右值引用只能绑定到临时对象,得知:所有引用对象将要被销毁,该对象没有其他用户。

   我们可以从绑定的右值“窃取”状态。

   变量是左值,不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。

10.标准库move函数:

    新标准库的move函数可以  获得绑定到左值上的右值引用。

    int &&rr2 = std::move(rr1);        // 告诉编译器我们有个左值,但希望像右值一样处理它

    调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它,调用move后,不能对移后源对象的值做任何假设。

    不提供using声明,直接调用std::move而不是move。这样做可以避免潜在的名字冲突。

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

    StrVec::StrVec(StrVec &&s) noexcept   // 不应抛出任何异常(在声明和定义中都要指定)

    :elements(s.elements),  first_free(s.first_free),   cap(s.cap){

        s.elememts = s.first_free = s.cap = nullptr;  }   

    源对象(s)必须不再指向被移动的资源(elements,first_free和cap),这些资源的所有权已经归属新创建的对象(this).

    忘记改变s.first_free,则销毁移后原对象会释放刚刚移动好的内存。

    如果希望在vector重新分配内存时,使用定义好的的移动构造函数而不是拷贝构造函数,则移动构造函数指定为noexcept。

    StrVec &StrVec::operator=(StrVec &&s) noexcept   // 不应抛出任何异常

    {

        if (this != s){

            free();

            elements = s.elements; first_free = s.first_free; cap = s.cap;

            s.elememts = s.first_free = s.cap = nullptr;  }   

        return *this;}

    移动源对象必须可析构。编写一个移动构造函数,必须保证移动源对象进入一个可析构的状态,StrVec使用赋值为nullptr的方式。

    移后的移动源必须保证是有效的。有效指可以安全地为其赋予新值或安全的使用而不依赖当前值。不应依赖移动源对象中的数据。

    编译器不会为某类合成移动操作。(一个类有自定义拷贝操作和析构函数,编译器就不会为它合成移动操作了)

    如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝函数来代替移动操作。

    只有一个类没有定义拷贝操作和析构函数,且所有数据成员都能移动操作,则编译器会合成移动构造函数或移动赋值运算符。

    移动操作定义为删除的情况:

  • 显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则会将移动操作定义为删除的函数。
  • 有类成员是const或引用,析构函数是删除或不可访问的

    如果移动操作可能被定义为删除的函数,编译器就不会合成它。

    如果类定义了一个移动构造函数或一个移动运算符,则该类的拷贝操作会被定义为删除的。

    右值使用移动,左值使用拷贝。但如果没有移动操作,右值也被拷贝(即使调用move,&&也会被转换成const &型进行拷贝)。

    HasPtr& operator=(HasPtr r){  swap(*this, r);  return *this;}      // 既是移动运算符也是拷贝运算符,因为参数是对象本身。

12.移动迭代器:

    移动迭代器适配器:通过改变给定迭代器的解引用运算符的行为来适配此迭代器。

    一般来说,一个迭代器的解引用运算符返回一个指向元素的左值,而移动迭代器的解引用运算符生成一个右值。

    auto moveiter = make_move_iterator(vec.begin);   // 参数为普通迭代器,返回一个移动迭代器。

    确定移动操作是安全的,移动源对象之后不会再使用,才能使用std::move。

13.右值引用和成员函数:

    旧标准中,左值可修改,右值不可。新标准右值也可修改。希望在类中阻止右值赋值的操作,使用以下方法。

    指出this是左值还是右值引用,参数列表后放置一个引用限定符。

    &限定的函数只能用于左值,&&只能用于右值。

    Foo &Foo::operator=(const Foo &r) &           // 只能向左值赋值(左值可修改)&&表示this的右值引用赋值

    { 将r赋值给this; return *this }

    const和引用限定符一起修饰一个函数时,引用限定符放在const后面。

    引用限定符也区分重载版本,&&和const & 是两个函数。

    如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值