第十三章 拷贝控制

版权声明:转载请注明出处 https://blog.csdn.net/weixin_39918693/article/details/86570148


拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符以及析构函数

拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称这些操作为拷贝控制操作

*注意是同类型

一些类可以使用默认的拷贝控制操作,一些类却不行。通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作


一、拷贝、赋值和销毁

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数是拷贝构造函数

一般来说都是const的引用,而且拷贝构造函数不应该是explicit的

即使我们定义了其他构造函数(没有定义拷贝构造函数),编译器也会为我们合成一个拷贝构造函数

一般而言,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中

虽然我们不能直接拷贝一个数组,但合成拷贝构造函数会逐元素的拷贝一个数组类型的成员

这里需要一个例子*

注意区分直接初始化和拷贝初始化

拷贝初始化通常使用拷贝构造函数来完成

如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成

拷贝初始化的发生的场合:

  • 1、用=定义变量
  • 2、将一个对象作为实参传递给一个非引用类型的形参
  • 3、从一个返回类型为非引用类型的函数返回一个对象
  • 4、用花括号列表初始化一个数组中的元素或一个聚合类中的成员

拷贝构造函数的形参为什么是一定要是引用:很有意思

如果我们希望使用一个explicit构造函数,就必须显式的使用

编译器可以绕过拷贝/移动构造函数

这里需要仔细看

注意拷贝构造函数的格式:p441

重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成

重载运算符的参数表示运算符的运算对象。某些运算符,包括赋值运算符,必须定义为成员函数。如果一个重载的运算符是一个成员函数,则左侧运算对象就绑定到隐式的this参数。对于一个二元运算符,其右侧运算对象作为显式参数传递

注意拷贝赋值运算符的格式:p444

为了于内置的赋值运算符保持一致,重载的赋值运算符通常返回一个指向其左侧运算对象的引用

有些类的合成的拷贝控制成员会起到禁止的作用

*这里需要在以后理清

合成的拷贝赋值运算符一般来说会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员

*非static成员

析构函数是类的一个成员函数,名字由波浪号接类名构成。没有返回值,也不接受参数。对于一个给定的类,只会有唯一一个析构函数。析构函数释放对象使用的资源,并销毁对象的非static数据成员

析构函数销毁的是对象的非static数据成员

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

析构函数的析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型

隐式销毁一个内置指针类型的成员不会delete它所指向的对象

*如果上述内置类型的指针指向一个动态内存,那么这样的隐式销毁会造成内存泄漏

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

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

??????对上述很是疑惑???????

析构函数体自身并不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象的销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的

有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数

如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符

一个类需要拷贝构造函数,那么其一定需要拷贝赋值运算符,这时却不一定需要析构函数

上述两句话很重要

我们可以通过将拷贝控制成员定义为=default来显式的要求编译器生成合成的版本

当我们在类内用=default修饰成员的声明时,合成的函数将隐式的声明为内联的(就像任何其他类内的声明的成员函数一样)。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default,就像对拷贝赋值运算符所做的那样。p449

???????上述这句话很是矛盾????????

我们只能对具有合成版本的成员函数使用=default(默认构造函数和拷贝控制成员)

在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数是这样一种函数;我们虽然声明了他们,但我们不能以任何方式使用他们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的。iostream类

阻止拷贝*

与=default不同,=delete必须出现在函数第一次声明的时候。我们可以指定任何函数为=delete的

析构函数不能是删除的成员

合成的拷贝控制成员可能是删除的。本质上,这些规则的含义是:如果一个类有数据成员不能默认构造、拷贝、移动或销毁,则对应的成员函数将被定义为删除的。(缺少底层的支持)p451

*合成的拷贝控制成员可能是删除的

声明但不定义一个成员函数是合法的

与delete声明等价的旧的方法,通过private来实现


二、拷贝控制和资源管理

管理类外资源的类必须定义拷贝控制成员

*类外资源

拷贝语义:定义拷贝操作,使类的行为看起来像一个值或者像一个指针

**上述很重要

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

拷贝赋值运算符异常安全的要求是当异常发生时能将左侧运算对象置于一个有意义的状态

类的拷贝赋值运算符需要考虑到自赋值的情况和异常安全。套路p454

定义行为像指针的类:底层数据共享

令一个类展现出类似于指针的行为的最好方法是使用shared_ptr来管理类中的资源

我们可以手动在动态内存中设置一个计数器。在定义拷贝控制成员的时候,还是要考虑到自赋值情况和异常安全。套路见p456


三、交换操作

除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。对于那些常常与重排元素顺序的算法一起使用的类,定义swap是非常重要的。

如果一个类定义了自己的swap,那么算法将使用类自定义的版本。否则,算法将使用标准库定义的swap。自定义版本性能较强

swap函数从理论上说,内存分配都是不必要的,可以通过交换指针来提高性能。我们一般使用标准库swap来实现自定义版本的swap

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

swap函数应该调用swap,而不是std::swap,在已经曝光std::swap的情况下,如果存在特定类型的swap,那么该版本优于标准库版本

在拷贝赋值运算符中使用swap,使用拷贝并交换的技术,该技术可以自动处理自赋值情况,而且天然异常安全。这是拷贝赋值运算符的另一种实现而已

拷贝赋值运算符的另一种实现


四、拷贝控制示例

示例程序看懂就回忆起来了


五、动态内存管理类

自己手动管理内存,需要定义拷贝控制成员。示例程序很简单

使用移动构造函数可以避免重新分配内存所导致的额外系统开销

对于标准库中类的移动构造函数来说,标准库保证“移后源”对象仍然保持一个有效的、可析构的状态

我们通过使用std::move函数来显式指出我们希望使用移动构造函数,std::move定义在utility头文件中。一般是直接使用,不会进行move的using声明

我们不知道移后源对象中包含什么值,但我们保证对他们执行析构函数是安全的。移动操作是一种移交内存块管理权限的操作。可以很可观的提高性能


六、对象移动

对象可移动可以为程序提供极大的便利。p470

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

所谓的右值引用就是必须绑定到右值的引用,其只能绑定到一个将要销毁的对象。因此,我们可以自由的将一个右值引用的资源移动到另一个对象中。&&

一般而言,左值表示的对象的身份,右值表示的是对象的值

我们不能将非const左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式上。右值引用可以绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上

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

右值引用所绑定的对象一般来说都是将要销毁的对象或该对象没有其他用户。这意味着使用右值引用的代码可以自由的接管其所引用的对象的资源

变量是左值,所以我们不能将右值引用绑定到一个右值引用类型的变量上

我们通过标准库move函数可以获得绑定到左值上的右值引用。对一个对象调用该函数的意思是说:我们有一个左值,但我们希望像一个右值一样处理它。

*****std::move可以获得绑定到左值上的右值引用,然后通过重载函数的匹配进而调用移动版本的拷贝控制成员

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

直接使用std::move,而不提供using声明,可以避免潜在的名字冲突

除了完成资源的移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源

移动操作不应该抛出异常,在参数列表后加noexcept对象进行强行阻止

移动操作不应该抛出异常

如果需要指定noexcept,声明和定义中都必须要标记

不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

要深入理解标准库是如何与我们自定义的类型进行交互的

标准库容器能对异常发生时其自身的行为提供保障。因为其有良好的定义

如果想让自定义的容器类和标准库容器类一样为自身的行为提供保障,想让其在重新分配内存时,使用移动构造函数而不是拷贝构造函数,必须显式的告诉标准库我们的移动构造函数可以安全的使用(即不会抛出异常)

移动赋值运算符和拷贝赋值运算符一样需要处理自赋值情况和保证异常安全

移后源对象必须可析构:对象有效就是指可以安全的为其赋予新值或者可以安全的使用而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据

在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设

如果我们不声明自己的拷贝构造函数和拷贝赋值运算符,编译器总会为我们合成这些操作。如果一个类定义了自己的拷贝成员函数和析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了

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

只有当一个类没有定义任何自己版本的拷贝控制成员,且他的所有非static数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符

与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。?????????

什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则

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

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

???????上述的总结有些混乱???????

如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似。例如,拷贝构造函数接受一个const StrVec的引用。因此,它可以用于任何可以转换为StrVec的类型。而移动构造函数接受一个StrVec&&,因此只能用于实参是(非static)右值的情形。(理解函数匹配过程)

如果没有移动构造函数,右值也被拷贝,用拷贝构造函数代替移动构造函数几乎肯定是安全的(赋值运算符的情况类似)。一般情况下,拷贝操作满足对应的移动操作的要求(移动是一种优化手段)

利用拷贝并交换技术实现的赋值运算符实现了拷贝赋值运算符和移动赋值运算符两种功能。由实参的类型决定使用哪一种功能(左值被拷贝,右值被移动)。一般是通过std::move()函数来显式指明的

注意拷贝并交换技术

一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作(杜绝所有磨棱良可的可能性)

新标准库中定义了一种移动迭代器适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值(引用)。也其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用

我们通过调用标准库的make_move_iterator函数来将一个普通的迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。原迭代器的所有其他操作在移动迭代器中都照常工作

标准库不保证哪些算法适用移动迭代器,哪些不适用,由于移动一个对象可能销毁原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法

在移动操作成员这些类实现代码之外的地方,只有当你确信需要进行移动操作且移动操作是安全的,才可以使用std::move

除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。这种允许拷贝/移动的成员函数通常使用与拷贝/移动构造函数和移动/拷贝赋值运算符相同的参数模式。你懂的,遵循习惯的形参模式

两种形参模式,形成了函数的重载

通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值

新标准为了向后的兼容性,仍然允许向右值赋值和用右值对象调用成员函数

我们可以在自定义的类中进行限定,通过指明this的左值/右值属性的方式来实现,具体是在参数列表后放置一个引用限定符

引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。和const限定符一样,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中

一个成员函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在const限定符之后

我们可以根据是否有const和引用限定符共同来形成成员函数的重载

const和引用限定符在重载形式上有略微区别p484

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

猜你喜欢

转载自blog.csdn.net/weixin_39918693/article/details/86570148
今日推荐