c++ primer 概念总结第十三章 拷贝控制


在本章中主要学习如何控制该对象的拷贝,赋值,移动和销毁之后需要做什么
我们通过一些特殊的函数来控制这些操作。拷贝构造函数移动构造函数拷贝赋值运算符移动赋值运算符,以及析构函数


13.1 拷贝,赋值与销毁

13.1.1拷贝构造函数

如果一个函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此函数就是拷贝构造函数
拷贝构造函数必须是引用类型,通常是const的,但不应该是explicit的。

合成拷贝构造函数

如果没有为类定义一个拷贝构造函数,编译器会为我们定义一个,与合成默认构造函数不同,即使我们定义了其他的构造函数编译器也会为我们合成一个拷贝构造函数。 因此对于某些类来说。 合成拷贝构造函数用来组织我们拷贝该类类型的对象(怎么理解?)。合成的拷贝构造函数将其参数的非静态成员逐个拷贝到正在创建的对象中
每个成员的类型决定了它如何进行拷贝。對类类型的成员,会使用其拷贝构造函数进行拷贝。内置类型的成员直接拷贝。虽然我们不能拷贝数组,但是合成的拷贝构造函数会逐个元素拷贝一个数组类型的成员。如果数组的元素是类类型,即使用元素的拷贝构造函数进行拷贝。

直接初始化和拷贝初始化

直接初始化时,实际是要求编译器使用普通的函数匹配。拷贝初始化要求将右侧对象拷贝到正在创建的对象中。 如果有移动构造函数,则拷贝初始化会使用移动构造函数,而非拷贝构造函数来完成。

拷贝初始化会在什么时候发生呢:
在使用等号定义变量的时候。
对象作为实参传递给一个非引用形参的时候。
从一个返回类型为非引用类型的函数返回一个对象。
用花括号列表初始化一个数组中的元素或一个聚合泪中的成员。
当使用标准库中的容器调用insert或push成员函数时。(与之相对,用emplace成员创建的元素都进行直接初始化)。

参数和返回值。

我们知道在这两种情况下都需要调用拷贝构造函数。这里解释了为什么拷贝构造函数必须使用引用类型的参数。如果不是因用类型的参数的话,就会陷入无限的循环当中。

拷贝初始化的限制

如果我们使用的初始化值要求通过一个expicit的构造函数进行类型转换。那么使用拷贝初始化和使用直接初始化就不是无关紧要的了。

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

编译器允许将下面的代码
string null_book = “9-999”;
改写为
string null_book(“9-999”);
编译器在这里略过了拷贝构造函数,但是拷贝构造函数必须是可见的。要不然编译是不会通过的。

13.1.2 拷贝赋值运算符

拷贝赋值运算符左侧绑定在隐式的this参数,右侧运算对象作为显式参数传递.返回值是一个指向左侧对象的引用.如果一个类位定义自己的拷贝赋值运算符,那么编译器会合成一个拷贝的版本.把非static成员赋予左侧运算对象的成员,而且这一操作是通过成员的赋值操作运算符完成的.对于数组元素逐个赋值.

13.1.3 析构函数

构造函数初始化话对象的非static成员.析构函数释放对象使用的资源.并销毁对象的非static成员.
析构函数不接受任何的参数,也没有任何的返回值,因此也不能被重载.
析构函数首先执行函数体,然后按照初始化的顺序逆序销毁数据成员.
析构函数什么时候被调用呢: 当变量离开作用域的时候;对象被销毁,成员也要被销毁的时候,或者容器被销毁,元素也被销毁时;指向动态内存指针主动调用delete函数的时候;临时对象创建它的完整表达式的时候.
指向一个对象的指针或者引用在离开作用域的时候不会执行析构函数.
当一个类未定义析构函数的时候,编译器会为其自动合成一个析构函数.
成员是在析构函数替执行后隐含的阶段被销毁的.

13.1.4 三五法则

定义的时候要考虑一个三五法则:
通常有三个基本操作可以控制类的拷贝操作:拷贝构造函数,拷贝赋值运算符,析构函数.一个类还可以定义一个移动构造函数,或者一个移动赋值运算符.有时候他们之间是一个整体,只定义一个而不需要定义其他的情况是少见的.
如果一个类需要定义析构函数,那么可以肯定它需要定义拷贝赋值运算符和拷贝构造函数.(这个例子在primer中需要好好读读).
需要拷贝操作的类也需要赋值操作,反之亦然.

13.1.5 使用=default

我们可以通过拷贝控制成员定义位default来显示的要求编译器生成合成的版本.如果我们在类内使用=default修饰成员,合成的函数将显示的生命为内联的.如果我们不希望我们的成员是内联的,需要在类外定义使用=default.

13.1.6阻止拷贝

合成的拷贝构造函数并不能适应真实的场景,我们需要定义自己的拷贝够函数来组织合成.iostream类就阻止了拷贝,以防多个对象读写相同的IO.
在新标准下,我们可以通过拷贝构造函数和拷贝赋值运算符定义为删除=delete的函数来阻止拷贝.删除的函数是这样的一种函数,虽然我们声明了他们,但不能以任何的方式使用他们.
default和delete构造函数有很多的不同,delete必须出现在函数第一次声明的时候.另一个不同之处在于delete可以用来指定任何的函数是删除的.
但是析构函数不能是删除的对于定义了删除构造函数的类,我们不能定义这种类型的对象或者成员,但是可以动态的分配这种类型的对象.
合成的拷贝构造函数也可能是删除的.特别是当不可能拷贝,赋值和销毁成员的时候,类的合成拷贝控制成员就被定义位删除的.
private 拷贝控制:在新标准发布之前,类通过将其拷贝构造函数和拷贝赋值运算符声明为private而阻止拷贝的.但是友元和和成员函数仍旧可以拷贝成员.为了阻止可以将控制成员声明位prvate的而不定义他们. 友元和成员函数试图访问这个函数可以造成链接错误.

13.2 拷贝控制和资源管理

行为像值的类

现在有一个类叫HasPtr,里面有一个int和一个指向string的指针.
每个对象都应该拥有一分自己的拷贝,为了实现类值行为,HasPtr应该定义如下行为:
定义一个拷贝构造函数,完成string的拷贝,而不是拷贝的指针
定义一个析构函数释放string
定义一个拷贝赋值运算符来释放对象当前string,并从右侧运算对象拷贝string
类值拷贝赋值运算符通常组合了析构函数和构造函数的操作.类似析构函数.赋值操作会销毁左侧运算对象的资源.类似拷贝构造函数,赋值操作会从右侧对象拷贝数据.不过我们要保证赋值运算是异常安全的.
比较以下两种赋值操作的实现
HasPtr & HasPtr::operator=(const HasPtr &rhs){
auto newp = new string(*rhs);
delete ps;
ps = newp;
i = rhs.i;
return *this;
}

HasPtr &HasPtr::operator=(const HasPtr & rhs){
delete ps;
ps = new string(*(rhs.ps));
i = rhs.i;
return *this;
}

如果出现自赋值,第二种实现就不对了

行为像指针的类

定义一个类最好的办法就是使用share_ptr来管理类中的资源.拷贝一个share_ptr会拷贝一个share_ptr所指向的指针.
share_ptr类自己记录有多少个用户共享它所指向的对象当没有用户使用对象的时候.share_ptr负责释放资源.
我们自己来设计引用计数
1)当初始化对象的时候,只有一个对象共享状态.因此计数器初始化为1.
2)拷贝构造函数不分配新的计数器.拷贝构造函数递增共享计数器.
3)析构函数递减计数器.如果计数器数目为0.析构函数释放状态.
4)拷贝赋值运算符递增右侧运算对象的计数器.递减左侧运算对象的计数器.如果左侧运算对象的计数器变为0.意味着共享状态没有用户了,拷贝赋值运算符必须销毁状态.
如何来保存引用计数呢. 解决的方法是将引用计数保存在动态内存当中.当创建一个新的对象的时候我们也分配一个新的计数器,当拷贝和赋值对象的时候,我们拷贝指向计数器的指针. 定义一个使用引用计数的类:
class HasPtr{

public:
HasPtr(const std::string &s = std::string());
ps(new string(s),i(0),use(new std::size_t(l))){}
HasPtr(const HasPtr &p):ps(p.ps),i(p.i),use(p.use){++*use;};
HasPtr& operator=(const HasPtr &);
~HasPtr();
private:
std::string *ps;
int i ;
std::size_t;
};

类指针的拷贝成员篡改引用计数.

HasPtr::~HasPtr()
{
if(–*use==0)
{
delete ps;
delete use;
}

}

HasPtr&HasPtr::operator=(const HasPtr &rhs){
++*rhs.use;
if(–*rhs.use==0)
{
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}

13.3 交换操作

如果一个我类定义了自己的swap函数,方法会使用类自己定义的swap函数,否则算法使用标准库使用的swap函数.正常定义的swap需要一次拷贝两次赋值.有些内存的分配是没有必要的.因此我们更希望交换的是指针.

class HasPtr{

friend void swap(HasPtr& , HasPtr &);
};
inline
void swap(HasPtr &lhs , HasPtr &rhs){

using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i , rhs.i);
}

void swap(Foo &lhs , Foo &rhs){
using std::swap;
swap(lhs.h , rhs.h);// 调用的是HasPtr类,但是没有被隐藏
}
尽管这里用到using std::swap,但是没有隐藏HasPtr这个函数,具体18.2.3会讲解
定义swap还会用其定义拷贝赋值运算符
HasPtr & HasPtr::operator=(HasPtr rhs){
swap(*this , rhs);
return *this;
}
此赋值的技术处理了自赋值情况并且是天然异常安全的.原因在于先拷贝一份右侧的对象然后再进行赋值

13.4 拷贝控制实例

13.5 动态内存管理类

如果每个添加元素的成员函数会检查是否右空间容纳更多的元素.如果右成员函数会在下一个可用的位置构造一个对象.如果没有可用空间,ector就会重新分配空间:它获得新的空间,将已有的元素拷贝到新的元素,释放旧空间,添加新元素.
我们用allocator来活得原始的内存.由于allocator分配的内存是未构造的.我们需要在添加新元素时候用construct成员在内存中创建对象.

移动构造函数和std::move()

reallocate成员

void StrVec::reallocate(){

auto newcapicity = size()?2*size():1;
auto newdata = alloc.allocate(newcapicity);
auto dest = newdata;
auto elem = elements;
for(size_t i =0 ; i!=size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
这个函数的move很有意思,string不会发生内存的拷贝,而是移交了内存的拥有权和管理权.我们保证执行string的西沟函数是安全.

13.6 对象移动

一方面可以节省资源,另一方面对于有些资源不能共享的类不能拷贝但是可以移动.在新标准中我们可以用容器保存不可拷贝的对象,但是可以移动.

13.6.1 右值引用

绑定在右值上的引用,只能绑定到一个将要销毁的对象上,因此我们可以自由的将一个右值引用的资源”移动”到一个对象中.
一般而言一个右值表达式表达的是一个对象的身份,而右值表达的是对象的值.左值不能将其绑定在要求转换的表达式,字面常量或是返回右值的表达式.右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上. 我们可以将一个const左值引用或者一个右值引用绑定到一个右值表达式上.

左值持久,右值短暂

左值有持久的状态,而且右值要么是字面常量,要么是在表达式求值过程中创建的临时对象.
由于右值引用只能绑定到临时对象,我们得知:
所引用的对象将要被销毁
该对象没有其他用户.
右值引用指向将要被销毁的对象.因此,我们可以从绑定到右值引用的对象”窃取”状态.

变量是左值.

move函数

move函数可以显示的将一个左值转换为对应的右值引用类型.调用move就意味着承诺:除了堆rrl赋值或销毁它外,我们将不再使用它,再调用move之后,我们不能对移后源做任何的假设.我们可以销毁移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值.

13.6.2 移动构造函数和移动赋值运算符

这两个成员对应拷贝操作,但他们从给定兑现窃取资源而不是拷贝资源.
移动构造函数的第一个参数是该类型的一个右值引用. 除了完成移动操作还必须保证移后源对象处于这样一个状态,销毁它是无害的.这些资源的归属权已经归属新创建的对象.明确点说就是,移动构造函数不分配任何内存.它给接管给定的对象内存,接管之后,它将给定对象中的指针都置为nullptr.

移动构造函数不会抛出异常.一种通知标准库的方法是指名 noexcept.是新标准引入的.我们虽然声明是不会抛出异常,但是函数还是可能会抛出异常,唯一好处是当函数发生异常的时候,不会执行栈回退,二是直接terminite终止.

移动赋值运算符与移动构造函数差不多,我们也要声明不会抛出任何异常.只不过我们要检查自赋值的情况.因为赋值号左边和右边可能指向的是同一个对象.

移后源对象必须可析构:移动操作之后,移后源必须保持有效,可析构的状态,但是用户不能对其值做任何的假设. 说了这么多就这么点事情.
合成的移动操作:合成移动操作与合成拷贝操作的条件大不相同,编译器根本不会为某些类合成移动操作,特别是,如果一个类的定义了自己的拷贝构造函数,拷贝赋值运算符,或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了,如果一个类没有移动操作,通过正常的匹配,类会使用拷贝操作代替移动操作 只有当一个类没有任何的自己版本的拷贝控制的时候,且类的每个非static数据成员都可以移动时候.编译器才会为它合成移动构造函数或移动赋值运算符.编译器可以移动内置类型的成员. 如果一个成员是类类型.且该类具有对应的移动操作.编译器也能移动这个成员.

与拷贝操作不同,移动操作永远不会隐式定义为删除的函数,如果我们显示的要求编译器生成=default的移动操作.且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数. 什么时候将移动构造函数和移动赋值运算符定义为删除的遵循与合成拷贝构造函数相似的原则.
1)当类成员定义了自己的拷贝构造函数且没有定义移动构造函数,或者类成员没有定义拷贝构造函数而且编译器不能为其合成移动构造函数.
2)如果有类成员的移动构造函数被定义为是删除的或者不可访问的.
3)如果类的析构函数被定义为删除的或者不可访问的,移动构造函数与移动赋值运算符也被定义为删除的
4)类似拷贝赋值运算符,如果有类成员是const或者是引用,则类的移动赋值运算符被定义位删除的.
回想以下移动构造与移动赋值干了哪些事情,他们无非是把一个对象的所有非静态成员都过度给新的对象,这个过程在拷贝赋值的过程中有三个步骤,生成一个临时的对象,赋值初始化或者覆盖对象原来的值.销毁临时对象.与拷贝赋值不同的是,没有什么临时对象,只不过将源对象所有成员的过度给新的对象. 似乎是个递归过程,本对象移动操作主要涉及到成员移动操作,因此必须成员是可移动的,本类的移动构造函数和移动赋值运算符才能够起效,否则就是被删除的. 而这递归终止的保证就是:内置类型是可以移动的,类成员也是可以移动的,可赋值的. 常量或引用的成员不可赋值. 如果已经定义了拷贝赋值运算符而没有定义移动构造函数,或者没有定义拷贝赋值运算符而且不能合成默认的移动操作,被定义位删除的.

如果类定义了一个移动构造函数或者一个移动赋值运算符,该类的合成构造函数和拷贝赋值运算符会被定义为删除的.

移动右值,拷贝左值,但是如果没哟移动构造函数,左值也被拷贝.如果类中有一个可用的拷贝构造函数而没有移动构造函数,那么拷贝构造函数代替移动构造函数的工作.实际上,拷贝构造函数甚至都不会改变原对象的值

移动迭代器

我们通过调用make_move_iterator 函数将一个普通的迭代器转换为移动迭代器,与其他的迭代器不同,移动迭代器解引用生成一个右值引用.
但是不要轻易的使用右值引用操作.除非你能确定移动源对象不再使用.

13.6.3 右值引用和成员函数

如果一个成员函数同时提供参数的拷贝和移动版本,它也能从中受益.但是通常我们不需要定义X& 和const X&&类型的参数.

右值和左值引用的成员函数.

有时候会有可以在左值基础上调用成员函数的现象,或者对一个右值进行赋值的情况.
这件事我们可以定义的,类似const限定符,我们可以在函数参数列表后面放置一个引用限定符来定义this的左值右值属性.引用限定符只能用于非静态成员函数,且必须同时出现在函数的声明和定义当中.
一个函数可以同时使用const 限定符和引用限定符号,但是引用限定符必须放在const限定符的后边.
引用限定符可以区分成员函数的重载版本.但是要注意 当我们定义const成员函数的时候,可以定义两个版本,唯一的区别是一个有const限定符一个没有,但是引用限定则不一样,我们定义两个具有相同名字相同参数的成员函数的时候,旧必须所有都加上函数限定符,或者都不加.这是有道理的,既然指明就要都指名了,如果不加又默认的this是什么类型的引用呢?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值