C++ primer chapter 13

拷贝 赋值 销毁

拷贝构造函数 如果一个构造函数第一个参数是自身的引用,而且任何额外参数都有默认值,则此构造函数是拷贝构造函数
拷贝构造函数的第一个类型必须是引用:如果参数不是引用类型,那么调用不会成功——为了调用拷贝构造函数我们必须拷贝他的实参,而拷贝实参又要调用拷贝构造函数

如果没有类定义拷贝构造函数,编译器会自动定义一个,和成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。对某些类来说,合成构造函数用来阻止我们拷贝该类类型的对象,而一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。(非static对象!)

拷贝初始化不仅仅在我们用=定义变量的时候发生,在下列情况下也会发生:
1.将一个对象作为实参传递给一个非引用类型的形参
2.从一个返回类型为非引用类型的函数中返回一个对象
3.用花括号列表初始化一个数组中的元素或者一个聚合类的成员

拷贝赋值运算符

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

编译器会对一个未定义自己拷贝运算符的类生成一个合成拷贝赋值运算符

析构函数和构造函数执行相反的操作:
析构函数释放对象使用的资源,并且销毁对象的非static类型数据成员。
在析构函数中首先执行函数体,然后销毁成员,成员按初始化的顺序逆序销毁

什么时候会调用析构函数:
变量在离开其作用域的时候
一个对象被销毁
容器被销毁
用于动态分配的对象,当对指向它的指针应用delete运算符的时候
对于临时对象,当创建它的完整表达式结束的时候

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

合成析构函数
当一个类没有定义自己的析构函数:编译器为它自动定义一个合成析构函数:成员并不再析构函数自身销毁,成员是在析构函数之后隐式的析构阶段中被销毁的。在整个对象销毁过程中,析构函数提是作为成员销毁步骤之外的一部分进行的。

 

三个基本操作可以控制类的拷贝操作:拷贝构造函数:拷贝赋值运算符,析构函数
通常这些操作被看作一个整体:需要析构函数的类也需要拷贝和赋值操作

当我们决定一个类是否要定义它自己版本的拷贝控制成员,一个基本原则是首先确定这个类是否需要一个析构函数。通常对析构函数的需求比对拷贝构造函数或者赋值运算符的需求更加明显。
比如Hasptr在构造函数中动态分配内存,而合成构造函数不会delete一个指针数据成员,所以这个类需要一个析构函数来释放构造函数分配的内存。
如果没有拷贝构造函数和拷贝赋值运算符:
假定有些函数简单拷贝指针成员,多个hasptr可能指向相同的内存,当f返回的时候,hp和ret都被销毁,在这两个对象上都会调用hasptr的析构函数,此析构函数会delete指针成员,导致指针被delete两次。或者调用指向已经释放内存的指针!

使用=default
我们可以通过将拷贝控制成员定义为default来显式要求编译器生成合成版本。
组织拷贝:可以通过把拷贝构造函数和拷贝复制运算符定义为删除的函数:我们虽然定义了它们,但是我们不能以任何方式调用他们。
析构函数不能是删除的成员:如果析构函数被删除那么无法销毁该类型的对象了,而且如果一个类的某一成员的类型删除了析构函数,那么整体也无法被销毁。

拷贝控制和资源管理:
通常管理类外资源的类必须定义拷贝控制成员,我们首先必须确定这个类型对象的拷贝语义,让类的行为看起来像是一个值或者一个指针。(每个对象都有一份自己的拷贝,shared_ptr类似指针的行为);

行为像值的类:每个对象都有一份自己的拷贝
hasptr需要:定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针(原来的释放了吗?!)
定义一个析构函数来释放string
定义一个拷贝复制运算符来释放当前的string并且从右侧对象拷贝string


类值拷贝赋值运算符

赋值运算符通常组合了析构函数和构造函数的操作,类似析构函数,复制操作会销毁左侧运算对象的资源,类似拷贝构造函数,赋值操作会从右侧运算符对象拷贝数据。先进性构造函数的工作,接下来和析构函数一样,我们delete当前ps指向的string,然后就只剩下拷贝指向新分配的string指针,以及从rhs中拷贝int。
两点需要注意:
1.如果将一个对象赋值给他自身,赋值运算符必须能正确工作
2.大多数赋值运算符组合了析构函数和拷贝构造函数的工作

如果先释放p指针,然后给p指针赋值是错误的,因为当赋值对象两边相同的时候,

定义行为像指针的类:拷贝指针成员不慎而不是它指向的string,最好方法是用shared_ptr来管事类中的资源,使用引用计数:
工作方式:
1.除了初始化对象外,每个构造函数还要创建一个引用计数,来记录有多少对象和正在创建的对象共享状态,但我们创建一个对象,只有一个对象共享状态,所以引用计数设置为1
2.拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器,拷贝构造函数和递增共享的计数器
3.析构函数递减计数器:指出共享状态的用户少了一个,如果计数器为0,那么析构函数释放状态
4.拷贝复制运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器,如果左侧运算对象的计数器为0,销毁状态

唯一的难点在 哪里存放引用计数。计数器不能直接作用hasptr对象的成员。

解决此问题的一种方式是将计数器保存在动态内存中,当创建一个对象我们也分配一个新的计数器,当拷贝或者赋值对象,我们拷贝指向计数器的指针。

交换操作

除了定义拷贝控制人员,管理资源的类通常还定义一个名为swap的资源,交换两个元素。
我们希望节省不必要的内存分配,交换指针而不是分配string的新副本。
定义swap的类中通常用swap来定义它们的赋值运算符,这些运算符使用了copy and swap的技术
rhs并不是一个引用,而是一个副本,再赋值运算符的函数体中,我们调用swap交换数据成员,此时左侧原来的指针放到副本中,在副本离开作用域的时候被释放。

拷贝控制示例:

虽然通常来说分配资源更需要拷贝控制,但资源管理并不是一个类需要定义自己拷贝控制成员的唯一原因,一些类也需要拷贝控制成员。
13.4 练习

动态内存管理类:
某些类需要在运行时分配可变大小的内存空间。这种类通常可以使用标准库容器来保存他们的数据。
vector的一个简化版本只用于string 被命名为strvec
设计:
将其元素保存在来内需的内存中,为了获得可接受的性能,vector预先分配足够的内存来村保存可能需要的更多元素。vector的每个添加元素的成员函数会检查是否有空间容纳更多的元素,如果有,成员函数会在下一个可用位置构造一个对象,如果没有vector就会重新分配空间,添加。

我们将使用一个allocator来获得院士内存,由于allocator分配的内存是尾构造的我们需要添加新元素的时候调用allocator的construct成员来在原始内存中创建对象,类似的当我们需要删除一个元素的时候,我们将使用destroy成员来销毁元素。
allocator类:操作
allocator<T> a;定义一个名称为a的allocator对象可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的,未构造的内存,保存n个类型为t的对象。
a.deallocate(p,n) 释放从T*指针p中开始的内存,这块内存保存的n个类型为T的对象,p必须是一个先前由allocate返回的指针,而且n必须是p创建时所要求的大小。用户必须在这块内存中使用destroy来销毁每个创建的对象
a.construct(p,args) p必须是一个类型为t*的指针,指向一块原始内存,arg被传递给类型为T的构造函数,用来在p指向的内存中构建一个对象
a.destroy(p) p为T*类型的指针,此算法对p指向的对象执行析构函数


每个StrVec有三个指针成员指向其元素所使用的内存:
elements 指向分配的内存中的首元素
first_free 指向最后一个实际元素之后的位置
cap 指向分配的内存末尾之后的位置
名为alloc 的静态成员,类型为allocator<string>,alloc成员会分配StrVec使用的内存,我们的类还有4个工具函数:
alloc_n_copy会分配内存,并拷贝一个给定范围内的元素
free 销毁构造的元素并释放内存
chk_n_alloc保证strvec至少有添加n个元素的空间,如果没有空间,会调用reallocate在内存用完的时候为Strvec分配新的内存。

复习:

uninitialized_copy(b,e,b2) 可以在未初始化内存中创建对象,从迭代器b和e指出的输出范围内拷贝元素到迭代器b2制定的尾构造的院士内存中,b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。

//在编写reallocate成员函数之前,我们思考一下要做的内容:
//为一个更大string数组分配内存
//在内存空间前一部分构造元素保存现有内容
//销毁原内存空间中的元素,释放这块内存
//应当尽量减少分配和释放string的开销
移动构造函数和std::move
通过使用新标准库引入的两种机制,我们就可以避免string的拷贝,首先有一些标准库比如string都定义了所谓的移动构造函数,关于string的移动和构造函数如何工作的细节,以及有关实现的任何细节未公开,但是移动构造函数通常是将资源从给定对象移动而不是拷贝到当前位置,而且我们知道标准库保准移动以后源仍然保持一个有效可析构的状态。
当reallocate在新内存中调用string的时候,我们必须调用move来表示希望使用string的移动构造函数,关于move我们需要了解两个关键点,首先当realocate在新内存中构造string,他必须调用move来表示希望使用string的移动构造函数。

reallocate成员,首先调用allocate分配新的内存空间,每次重新分配内存都会将strvec的容量加倍如果strvec为空,将分配容纳一个元素的空间。
为什么在construct中使用后之地增运算如果使用前置递增会发生什么?
因为first_free指向第一个扼控线位置也就是最后一个string的后一个位置,当添加新的string的时候,应该保存在first_free指向的位置,然后将first_free推进一个位置,因此后置递增运算符符合要求。


对象移动
新标准一个最主要的特性就是可以移动而非拷贝对象的能力,在分配新内存的过程中有时候拷贝是不必要的,更好的方式是移动元素。
右值引用
为了支持移动操作,引入了一种新的引用类型,右值引用,&&来获得,它只能绑定到一个即将要销毁的对象,因此我们可以自由将一个右值引用的资源移动到另一个对象中。
类任何引用,一个有值引用也不过是某个对象的一个名字而已,对于常规引用,我们不能将它绑定到要求转换的表达式或者子方面常量,但是右值函数可以,只是不能绑定到一个左值上。采用有值引用的代码可以自由的接管所引用的对象和资源。

变量是一个左值,不能绑定到优质引用。
移动构造函数和移动赋值操作符
类似string类,如果我们自己的类也同时支持移动和拷,那么也能从中受益,类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个有值引用,任何额外的蚕食都必须有默认实参。除了完成资源移动,还要保证移动后源对象处于无害的状态。它不分配任何新的内存,接管StrVec中对象的移动操作,对象将会继续存在(它将元对象中的指针全部设置为Nullptr).
移动操作不会抛出任何一场,noexcept承诺一个函数不跑出任何一场,在一个函数的参数列表后面制定noexcept,在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之前。
我们需要指出一个移动操作不跑出一场,这是因为:1.虽然移动操作不跑出异常,但是抛出异常也是允许的。2.标准库容器能对异常发生时自身恩德行为提供保障。如果vector使用了拷贝构造函数并且发生了异常,它可以很容易地满足要求。当在内存中构造元素的时候,旧元素保持不变,vector可重新分配内存并且返回。使用移动构造函数就会产生问题,因为移动一个对象通常会改变它的值。
移动赋值运算 执行 析构函数+移动构造函数

移动后源对象必须可析构:从一个对象移动数据并不会销毁此对象,但是有时在操作完成后,源对象会被销毁,因此,当我们编写一个移动操作,必须确保移动后原对象进入一个可以析构的状态。(这里通过将移动后的元对象设置nullptr)

合成的移动操作:如果我不生成自己的copy构造函数uhe拷贝赋值运算符,编译器会为我们合成一个,但是编译器不会为某些类合成移动操作,特别当一个函数已经有了自己的拷贝赋值运算符。
只有当一类没有定义任何自己版本的拷贝控制成员,而且类的没有给非static数据成员都可以移动的时候,编译器才会为它合成移动构造函数或者移动赋值运算符。

与copy操作不同,移动操作永远不会隐式定义为删除ude函数,但是如果我们显示要求生成=default,且编译器不能移动所有成员,那么编译器会将移动操作定义为一个删除的函数相互。定义了一个移动构造函数或者移动赋值运算的类必须也定义自己的拷贝操作。
移动right,拷贝left

如果没移动构造函数,只有拷贝构造函数,那么只会被copy,即使调用std::move

拷贝并交换赋值运算符和移动操作
移动赋值运算符也可以实现先交换然后利用离开作用域进行析构!

message类可以使用string和set的移动操作来避免contents和folders成员的额外开销。

 

posted @ 2017-04-25 21:03  joeylee97  阅读(188)  评论(0编辑  收藏  举报