美文网首页C++程序员
[C++ Primer Note12] 拷贝,赋值与销毁

[C++ Primer Note12] 拷贝,赋值与销毁

作者: 梦中睡觉的巴子 | 来源:发表于2018-11-27 23:23 被阅读17次

    当定义一个类时,我们显式地或隐式地制定在此类型的对象拷贝,移动,赋值和销毁时做什么。一个类通过五种特殊的成员函数来控制这些操作,包括:拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数。我们称这些操作为拷贝控制操作(copy control)

    1. 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。通常这个参数都是const的,并且该函数不应该是explicit的。
    2. 如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。
    3. 一般情况下,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。对类类型的成员,会使用其拷贝构造函数来拷贝,内置类型的成员则直接拷贝。虽然我们不能拷贝一个数组,但合成拷贝构造函数会逐个元素地拷贝一个数组类型的成员。
    4. 当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配。当使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中。
    5. 拷贝初始化不仅在我们使用=定义变量时会发生,在下列情况下也会发生:
    • 将一个对象作为实参传递给一个非引用类型的形参
    • 从一个返回类型为非引用类型的函数返回一个对象
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
    1. 与类控制其对象如何初始化一样,类也可以控制其对象如何赋值:
    Sales_data trans,accum;
    trans = accum;  //使用拷贝赋值运算符
    

    如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。

    1. 重载运算符本质上是函数,其名字由operator关键字后接要定义的运算符的符号组成。因此,赋值运算符就是一个名为operator=的函数。类似于其它函数,运算符函数也有返回值和参数列表。
    2. 重载运算符的参数表示运算符的运算对象。某些运算符,包括赋值运算符,必须定义为成员函数。如果一个运算符是成员函数,其左侧运算对象就绑定到隐式的this参数。对于一个二元运算符,其右侧运算对象作为显式参数传递。
    3. 赋值运算符通常返回一个指向其左侧运算对象的引用
    Sales_data& Sales_data::operator=(const Sales_data &rhd){
        bookNo=rhs.bookNo;
        units_sold=rhs.units_sold;
        revenue=rhs.revenue;
        return *this;
    }
    

    如果一个类未定义自己的拷贝赋值运算符,编译器会为它合成一个。

    1. 析构函数执行与构造函数相反的操作,释放对象使用的资源,并销毁对象的非static数据成员。析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数,因此也不支持重载。
    2. 在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。析构部分是隐式的,成员销毁时发生什么完全依赖于成员的类型
    3. 无论何时一个对象被销毁,就会自动调用其析构函数:
    • 变量在离开作用域时被销毁
    • 当一个对象被销毁时,其成员被销毁
    • 容器被销毁时,其元素被销毁
    • 对于动态分配的对象,当对指针应用delete时被销毁
    • 对于临时对象,当创建它的完整表达式结束时被销毁
    1. 析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。
    2. 隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
    3. 如前所述,已经有三个基本操作可以控制类的拷贝操作,在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符,C++并不要求我们定义所有这些操作。
    4. 当我们决定一个类是否要定义自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数。通常,对析构函数的需求比对拷贝构造函数或赋值运算符的需求更明显。如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。因为合成析构函数不会delete一个指针数据成员,所以有时候需要定义一个析构函数,但同时如果采用合成拷贝构造函数和合成拷贝运算符时就会出现很多问题,因为合成的方法仅仅拷贝指针的值,而不是拷贝对象。
    5. 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然。但无论需要拷贝构造函数还是拷贝赋值运算符不意味着也需要析构函数。
    6. 我们可以通过将拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本,当我们在类内声明时,将隐式地声明为内联的。如果不希望如此,应该只对成员的类外定义使用=default。
    7. 对某些类来说,拷贝构造函数和拷贝赋值运算符没有意义。在此情况下,定义类时必须采用某种机制阻止拷贝或赋值。例如,iostream类阻止了拷贝,避免多个对象写入或读取相同的IO缓冲。为了阻止拷贝,看起来应该不定义拷贝控制成员,但是这种策略恰恰是无效的,如前文所述编译器会生成合成的版本如果我们没有定义它们。
    8. 在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function)来阻止拷贝 。删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的:
    struct Nocopy{
        Nocopy() = default;
        Nocopy(const Nocopy &) =delete;       //阻止拷贝
        Nocopy& operator= (const Nocopy &) =delete;      //阻止赋值
        ~Nocopy() = default;
    }
    
    1. =delete必须出现在函数第一次声明的时候,这与=default不同。我们可以对任何函数指定=delete
    2. 我们不能删除析构函数,对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针,但是可以动态分配这种类型的对象。
    3. 如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数将被定义为删除的。特别需要注意引用类型成员和const成员,具体规则此处不赘述。
    4. 在新标准发布之前,类是通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝的,但现在已经不采用这样的方法。
    5. 当编写赋值运算符时,如果将一个对象赋予它自身,赋值运算符必须正确工作;同时大多数赋值运算符组合了析构函数和拷贝构造函数的工作
    6. 新标准的一个最主要的特性是可以移动而非拷贝对象的能力。很多情况都会发生对象拷贝,在其中某些情况中,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
    7. 为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference),所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象。
    8. 我们可以将一个右值引用绑定到要求转换的表达式,字面常量或是返回右值的表达式上,但不能将一个右值引用直接绑定到一个左值上:
    int i=42;   
    int &r=i;
    int &&rr=i;   //错误
    int &r2=i*42;   //错误:i*42是一个右值
    const int &r3=i*42;  // 正确:我们可以将一个const引用绑定到一个右值上
    int &&rr2=i*42;  //正确:将rr2绑定到乘法结果右值上
    

    返回非引用类型的函数,连同算数,关系,位以及后置递增/递减运算符,都生成右值。我们可以将一个const的左值引用或将一个右值引用绑定到这类表达式上。

    1. 右值引用只能绑定到临时对象,所引用的对象将要被销毁,该对象没有其它用户。
    2. 虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中:
    int &&rr3=std::move(rr1);
    
    1. 为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。
    2. 类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用,但这个引用参数必须是一个右值引用。除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。
    StrVec::StrVec(StrVec &&s) noexcept   //移动操作不应抛出任何异常
    :elements(s.elements),first_free(s.first_free),cap(s.cap){
        s.elements=s.first_free=s.cap=nullptr;
    }
    

    移动构造函数不分配任何新内存,它接管给定的StrVec中的内存。最终,移后源对象会被销毁,意味着在其上运行析构函数。

    1. 由于移动操作通常不分配任何资源,所以移动操作通常不会抛出任何异常noexcept是我们承诺一个函数不抛出异常的一种方法,我们必须在类头文件的声明中和定义中都指定noexcept。
    2. 移动赋值运算符执行与西沟函数和移动构造函数相同的工作,参数同样应该是右值引用类型。
    3. 在移动操作之后,移后源对象必须保持有效,可析构的状态,但是用户不能对其值进行任何假设
    4. 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员,如果一个成员是类类型,且该类由对应的移动操作,编译器也能移动这个成员。
    5. 如果类定义了一个移动构造函数或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的,必须自己定义。
    6. 如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作情况类似。
    7. 除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中收益,这种允许移动的成员函数使用与拷贝/移动构造函数和赋值运算符相同的参数模式——一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值引用。
    8. 通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是右值:
    string s1="a value",s2="another";
    auto n =(s1+s2).find('a);
    

    新标准库类仍然允许向右值赋值,但是,我们可能希望在自己的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(this指向的对象)是一个左值。
    我们指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后面放置一个引用限定符:

    Foo &Foo::operator=(const Foo &rhs) &{
        ...
    }
    

    对于&限定的函数,我们只能用于左值,对于&&限定的函数,只能用于右值。
    一个函数可以同时用const和引用限定,在此情况下,引用限定符必须跟随在const后面

    1. 引用限定符也可以区分重载版本,如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符

    相关文章

      网友评论

        本文标题:[C++ Primer Note12] 拷贝,赋值与销毁

        本文链接:https://www.haomeiwen.com/subject/qpgnqqtx.html