文章目录
前言
c++primer第十三章学习笔记
一、拷贝、赋值与销毁
拷贝控制操作整体分为:拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值运算符和析构函数
最基本的是拷贝构造、拷贝赋值和析构函数
1、拷贝构造函数
Name(const Name &);//拷贝构造函数
参数为const 引用,并且该函数非explicit,即允许隐式转换。如果不定义,会生成合成拷贝构造函数,能够将参数成员逐个进行拷贝。通常发生在用=定义变量、实参传递给非引用形参、函数返回非引用对象以及花括号初始化数组元素或类中成员时。
形参和返回值都是需要将对象拷贝到内存中,因此需要执行拷贝构造函数,如果该函数不使用&类型作为参数,则会陷入无限递归中。
2、拷贝赋值运算符
拷贝赋值运算符返回一个指向其左侧运算对象的引用 Name & operator=(const Name &rhs){return *this};
当未定义时,系统自动合成一个拷贝复制运算符,个人理解是将右侧的对象进行拷贝,再将该拷贝结果赋值给左侧对象,该目的是用于保证 a=a 的系统安全。如果不拷贝,在a对象含有指针成员时,可能会将指针对象释放,造成空指针。
3、析构函数
~Name();
析构函数并不负责控制成员如何销毁,而是在成员销毁之前完成一些收尾工作。
注意:隐式销毁内置指针类型成员不会delete所指向的对象,因此最好使用智能指针
4、法则
由于析构的需求比拷贝和赋值要低,因此需要定义析构函数时往往需要定义拷贝构造函数和拷贝赋值函数(三原则)
需要拷贝构造时,也需要赋值,但不一定需要析构函数
可以使用=default显式地要求编译器生成合成的版本,不定义时会自动合成,可以使用=delete阻止某些函数被调用,析构函数除外。在c++11之前,通过将拷贝构造等函数设为private成员进行限制
二、拷贝控制和资源管理
有两种选择:类值、类指针
1、类值
对于此方式,在执行拷贝赋值操作时,应该是对值进行更新,如果含有指针,需要将指针对象释放,并new。但是考虑到自等于的情况,因此需要借助一个中间对象,否则会造成空指针
Class T{
private:
std::string *ps;
public:
T& operator=(const T& rhs){
auto temp = new string(*rhs.ps);
delete ps;//释放当前对象,若无前一步,a=a时右侧a.ps的对象已被释放,无法再赋值给左值a,造成空指针
ps = temp;
return *this;
}
};
2、类指针
借助共享指针的思路,使用计数器作为引用计数,当其值为0时,才进行删除,同样使用指针实现
Class T{
pubilc:
T(const std::string &s = std::string()):
ps(new std::string(s)),i(0),use(new std::size_t(1)){}
T(const T&t):ps(t.ps),i(p.i),use(p.use),{++*use;}
T& operator=(const T& rhs);
~T();
private:
std::string *ps;
int i;
std::size_t *use;
};
T::~T(){
if(--*use==0){//当引用计数为0,删除指针
delete ps;
delete use;
}
}
T::T& operator=(const T& rhs){
++*rhs.use;//右侧对象引用+1
if(--*use==0){//左侧对象引用-1。此时可能需要删除指针
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
三、交换操作
如果交换两个对象的值,需要借助一个中间对象,需要额外的构造和析构过程,可以对类自定义交换操作,避免产生中间对象。
Class T{
friend void swap(T &,T &);
};
inline void swap(T &lhs,T &rhs){
using std::swap;//引入标准命名空间,此时会优先匹配自定义版本
swap(lhs.ps,rhs.ps);//借助标准库的函数进行交换
}
可以借助swap定义拷贝赋值操作,形参不再是引用类型。
T& operator=(const T t){
swap(*this,t);
reutrn *this;
}
四、对象移动
对于拷贝而言,会创建一个可被使用临时对象,需要额外空间,对于某些拷贝操作而言,可以使用移动,借助指针的修改,直接将值赋给左值,通常用于保存对于一些临时值和非引用的函数返回值。
左值使用其身份,右值使用其值,可以使用右值引用保存临时值或即将被销毁的对象,再通过move窃取其值,就能够避免拷贝。
1、移动构造函数和移动赋值运算符
Name(Name &&)noexcept;
Name& operator=(Name &&)noexcept;
与拷贝构造函数类似,移动函数的第一个参数是右值引用,其他参数需要默认参数。由于移动操作通常不会抛出任何异常,需要在参数列表后指明noexcept,避免标准库产生额外工作处理可能性异常。
注意:移后源对象必须是可析构或者是可赋新值的,但是仍然可能保留值,只不过不能再使用
2、合成移动函数
合成该函数的条件比较高,只有不存在自定义的拷贝函数,以及类的每个非static成员都可以移动时,才会生成合成移动函数。如果使用=default将其显式生成,但存在成员无法移动时,会被定义为delete的函数
当移动和拷贝同时存在时,会根据函数匹配规则自动选择。
由于移动会使得移后源对象不再直接访问,因此需要格外注意移动操作的安全性.
3、右值引用和成员函数
成员函数同样可以有两种版本,指向const的左值引用和指向非const的右值引用,会分别执行拷贝和窃取。因此没必要设置非const的左值引用和const的右值引用。
可以对成员函数限制其为左值或者右值才可调用,在参数列表后加&或者&&,该方式也可以重载,但必须是同时用或者同时不用
Type Name() &;//对象为左值可调用该函数
Type Name() &&;//右值可调用
Type Name() const &;//如果函数是const的,&必须在const之后
Type a;
a.Name();//调用左值版本
std::move(a).Name();//调用右值版本
总结
拷贝控制分为五类,最基础的是拷贝构造和拷贝赋值,其次是是移动构造和移动赋值,最后是析构。移动通常接受一个非const的右值引用,拷贝通常接受一个const的左值引用。