拷贝控制总结

1.拷贝、值与销毁:

拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且其他(如果有的话)参数都有默认实参,则此构造函数叫做拷贝构造函数;如果我们没有为类定义一个拷贝构造函数,那么编译器就会为我们定义一个合成拷贝构造函数。

拷贝初始化:

直接初始化与拷贝初始化的差异:当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,而不是直接将值赋予正在创建的对象,是先构造一个临时对象,然后将其拷贝到正在创建的对象,如果需要的话还要进行类型转换。

拷贝初始化不仅在我们用 “ = ”定义变量时会发生,在下列情况中也会发生:

        (1)将一个对象作为实参传递给一个非引用类型的形参;

        (2)从一个返回类型为非引用类型的函数返回一个对象;

        (3)用花括号列表初始化一个数组中的元素或一个聚合类中的成员。

与拷贝构造函数对应的就是拷贝赋值运算符:

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

合成拷贝赋值运算符:如果类未定义一个拷贝赋值运算符,那么编译器就会为其生成一个合成拷贝复制运算符。

析构函数:析构函数是类的一个成员函数,无返回值,不接受参数,因其不接受参数,所以其不能被重载,对于一个给定类,只会有唯一一个析构函数。

在一个析构函数中,不存在类似构造函数中初始化列表中的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。隐式销毁一个内置指针类型的成员不会delete它所指向的对象。与普通指针不同,智能指针是类类型,所以具有析构函数。因此智能指针成员会在析构阶段自动销毁。

什么时候回调用析构函数(认为非常重要):

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

        (1)变量在离开其作用域时被销毁;

      (2)当一个对象被销毁时,其成员被销毁;

      (3)容器被销毁时,其元素被销毁;

        (4) 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;

      (5)对于临时对象,当创建它的完整表达式结束时被销毁。  

同样为定义析构函数,编译器也会自动生成合成析构函数。

三 / 五法则:有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数。在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符。

当我们决定一个类是否要定义他自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数。如果需要,则几乎可以肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。

合成的析构函数不会delete一个指针数据成员(指针所指对象),因此此类型的类需要定义一个析构函数。需要拷贝操作的类也需要赋值操作,反之亦然。

我们可以通过使用=default定义拷贝控制成员来显示的要求编译器生成合成的版本。当我们在类内用=default修饰成员的声明时,合成的函数将隐式的声明为内联的(像任何其他类内声明一样),如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default。我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)。

定义删除的函数:我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数(delete function)来阻止拷贝。删除的函数:我们虽然定义了它,但不能以任何方式使用它。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的,必须出现在该函数的第一次声明时。

注意析构函数是不能定义成删除的成员:如果析构函数被删除,就无法销毁此类型的对象了,对于一个删除了析构函数的类,编译器将不允许定义该类型的变量或创建该类型的临时对象,但是可以动态分配这种类型的对象,不能释放指向该类型动态分配对象的指针。

对于某些类来说,编译器将这些合成的成员定义为删除的函数:

1、如果类的某个成员的析构函数是删除的或者不可访问的(例如是private的),则类的合成析构函数被定义为删除的;

2、如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的。如果类的某个成员的析构函数是删除的或者不可访问的,则该类合成的拷贝构造函数也被定义为删除的;

3、如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或者类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的;

4、如果类的某个成员的析构函数是删除的或者不可访问的,或是类有一个引用成员,它没有类内初始器,或是有一个const成员,它没有类内初始器且其类型未显示定义默认构造函数,则该类的默认构造函数被定义为删除的。

综上:如果一个类内成员的“三’是缺失的或不可访问的则类的相关合成的“三”会被定义为删除的。

类内成员函数可以只声明不定义,可以将拷贝赋值运算符和拷贝构造函数声明为private的,当未定义时可以阻止任何拷贝该类对象的企图:试图拷贝对象的用户代码将在编译阶段被标记为错误;成员函数或友元函数中的拷贝操作将会导致链接时错误。

2.拷贝控制与资源管理:

当编写赋值运算符时,有两点需要记住:

1、如果将一个对象赋予它自身,赋值运算符必须能正确工作(一个好的办法是在销毁左侧运算对象之前,拷贝右侧运算对象);

2、大多数赋值运算符组合了析构函数和拷贝构造函数的工作。

当编写一个赋值运算符时,一个好的模式是将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了。一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员了。

3.交换操作:

对于拷贝控制成员不同,swap并不是必要的,但是 对于分配了资源的类,定义swap可能是一种很重要的优化手段。本节主要内容为swap的定义与应用。

4.拷贝控制实例:

本节为Message类和Folder类的定义来处理消息,Message为一个存放信息的类,Folder为保存Message的类,Message类也有一个变量存放本信息存放于那些Folder中。我认为最难得一点就是同步处理Message和Folder。

5.动态内存管理类:

某些类需要在运行时分配可变大小的内存空间。这种类通常可以使用标准库容器来保存它们的数据,但是某些类需要自己进行内存分配,这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。默认定义三个指针成员指向其元素所使用的内存:elements,指向分配的内存中的首元素;first_free,指向最后一个实际元素之后的位置;cap,指向分配的内存末尾之后的位置。经常能用到的算法:construct(p,args)(p必须是一个类型为调用对象所存对象相同的指针,指向一块原始内存;args被传递给类型为调用对象的构造函数,用来在p所指的内存中构造一个对象),allocate(n)(分配一段原始的、未构造的内存,保存 n 个类型为调用对象所存对象类型的对像),chk_n_alloc()确保有空间容纳新元素

对于移动构造函数move和reallocate比较难懂与重要:

首先:move是一个标准库函数,构造移动构造函数时,需要调用move来表示希望使用string或其他类型的移动构造函数,如果漏掉了move的调用,将会使用string或其他类型的拷贝构造函数,当我们使用move时,直接调用std::move而不是move。

6.对象移动:

右值引用:右值引用有一个重要的性质:只能绑定到一个将要销毁的对象,因此,我们可以自由的将一个右值引用的资源“移动”到另一个对象中。

左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。右值引用可以将其绑定到要求转换的表达式(类型,形式)、字面值常量、或是右值的表达式,跟左值引用有着完全相反的绑定特性,但是不能将一个右值引用直接绑定到一个左值上。

由于右值只能绑定到临时对象:1、所引用的对象将要销毁;2、该对象没有其他用户。

这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

变量是左值:可以看作一个只有运算对象而没有运算符的表达式;

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

虽然不能直接将一个右值引用绑定到一定左值上,但是我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的标准库函数来获得绑定到左值上的右值引用。move告诉编译器:我们有一个左值,但是我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对此左值赋值或销毁它外,我们将不在使用它。在调用move函数后,我们不能对移后源对象的值做任何假设。与大多数标准库函数的使用不同,对于move不通过using声明,直接调用std::move而不是move。关于其具体工作机制将会在第十六章(模版与泛型编程)总结。

对于移动操作,除非标准库直到我们的移动构造函数不会抛出异常,否则他它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而坐一些额外的工作,通过在形参列表后写关键字“noexcept”来通知标准库我们的构造函数不会抛出任何异常。不抛出异常的移动构造函数和移动赋值运算符必须标记为“noexcept”。因为标准库容器能对异常发生时其自身的行为提供保障。

与拷贝操作不同编译器根本不会为某些类合成移动构造函数。特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器将不会为他合成移动构造函数和移动赋值运算符了。只有当一个类没有定义任何自己版本的拷贝控制函数,且类的每个非static数据成员都可以移动时,编译器才会为他合成移动构造函数或者移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。

与拷贝操作不同,引动操作永远不会隐式地定义为删除的函数。但是如果我们显示地要求编译器生成=default的移动操作,且编译器不能移动所有成员时,编译器会将移动操作定义为删除的操作。

定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的。

所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

移动迭代器:解引用运算符生成一个右值引用,通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。

对于移动操作的使用要谨慎。

右值和左值引用成员函数:我们指出this的左值/右值属性的的方式与定义const成员函数相同,即在参数列表后放置一个引用限定符,对于&限定的函数,我们只能将它用于左值;对于&&限定的函数,只能用于右值。一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟在const之后。const和引用限定符都可以用来区分重载,但,如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。明天更新下一章节。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lucky登

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值