this;
}
private:
int *_pa = nullptr;
};
13.1.5使用=default
在c++11中,我们可以通过把函数声明为default来把函数声明为使用系统默认合成的版本。当然我们只能对具有合成版本的成员函数使用=default。=defalut可以出现声明或者定义处。要特别注意的一点是,当我们在类内用=default修饰成员的声明时,合成的函数将隐式声明为内联函数。
class Test
{
public:
Test() = default;
~Test() = default;
Test(const Test &) = default;
Test &operator= (const Test &t) = default;
private:
int *_pa = nullptr;
};
13.1.6阻止拷贝
在c++11中,我们可以通过将拷贝控制函数声明为删除的函数来阻止拷贝。删除的函数是这样的一种函数:我们虽然声明了它,但是我们不可以以任何方式使用它。我们通过=delete来声明删除的函数。
与=default不同的是,=delete必须出现在函数第一次声明的时候,且不可以出现在定义中。另外,我们可以指定任何函数为删除的函数。
关于拷贝控制有以下几个原则:
值得注意的是,我们不能删除析构函数,因为这样的话对象将无法正常销毁。 如果一个类有数据成员不能默认拷贝、构造、赋值、销毁,那么该类对应的成员函数将被默认定义为删除的。 如果一个类有const成员,则它不能使用合成的拷贝赋值运算符,因为将一个新值赋值给const对象是非法的。
在c++11之前,阻止拷贝控制的方法是将相应的成员函数声明为private并且不定义它。但是c++11之后可以直接使用delete关键字进行声明。
13.2拷贝控制和资源管理
通常,管理类外资源的类必须自定义拷贝控制成员。
在定义拷贝控制成员时,通常有两种选择:使类的行为看起来像一个指针或者是类的行为看起来像一个值。
类的行为像一个值,意味着每个对象都有自己的状态。当我们拷贝一个对象时,副本和原对象应该是完全独立的。改变副本不会影响原对象的值,反之亦然。比如标准库中的string类。 类的行为像一个指针,则对象之间应该应该共享状态,副本和原对象应该使用相同的底层数据,改变副本也会改变相应的原对象,反之亦然。这种对象的实现多通过引用计数,比如标准库中的shared_ptr类。
13.2.1行为像值的类
在定义行为像值的类时,要特别注意拷贝赋值运算符的定义,其必须注意一下两点:
如果一个对象赋予它自身,赋值运算符必须可以正常工作 大多数赋值运算符组合了析构函数和拷贝构造函数的工作 如果有可能,我们编写的赋值运算符还应该是异常安全的——当异常发生时能将左侧对象置于一个有意义的状态
示例如下:
class Val
{
public:
Val(const std::string &s = std::string()) :ps(new std::string(s)){}
Val(const Val &v) :ps(new std::string(*(v.ps))){}
//错误的写法,无法处理自赋值,好的模式是先将右侧运算对象拷贝到一个临时局部对象中
Val &operator=(const Val &p)
{
delete ps;
ps = new string(*p.ps); //如果是自赋值,此时ps指向删除的内存,引用此指针将报错
return *this;
}
//特别注意正确赋值构造函数的写法
Val &operator=(const Val &p)
{
auto newp = new (std::nothrow) std::string(*p.ps);
if (newp)
{
delete ps;
ps = newp;
}
else //异常安全,如果内存分配失败,则不做任何处理
{
delete newp;
}
return *this;
}
~Val(){ delete ps; }
private:
std::string *ps = nullptr;
};
13.2.2定义行为像指针的类
令一个类展现类似指针的行为的最好的方法是使用shared_ptr来管理类中的资源,但是有时我们希望直接管理资源,这种情况下我们通常使用引用计数。
引用计数的工作方式如下:
除了初始化对象外,每个构造函数还要创建一个引用计数,用来记录有对象对象和正在创建的对象在共享状态。 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器,然后拷贝对象函数递增共享的计数器。 析构函数会递减计数器,如果计数器变为0,则析构函数将释放资源。
拷贝赋值运算符,将递增右侧运算对象的计数器,递减左侧对象的计数器,如果左侧对象的计数器变为0,那么将销毁左侧对象。
在实现过程中我们也要注意拷贝赋值运算符的必须要能处理自赋值的情况,所以我们要先递增右侧运算对象的引用计数,然后递减左侧运算对象的引用计数。
示例如下:
class HasPtr
{
public:
//默认构造函数中将引用计数初始化为1
HasPtr(const string &s = string()) :ps(new std::string(s)), pUser(new size_t(1)){}
//拷贝构造函数中将引用计数加1
HasPtr(const HasPtr &p) :ps(p.ps), pUser(p.pUser){ ++ *pUser; }
//特别注意拷贝赋值操作符的写法
HasPtr& operator=(const HasPtr &rhs)
{
//递增右侧对象的引用计数
++*rhs.pUser;
//递减左侧运算对象的引用计数,如果为0,则删除相应的资源
if (--*pUser == 0)
{
delete pUser;
delete ps;
}
ps = rhs.ps;
pUser = rhs.pUser;
return *this;
}
private:
std::string *ps = nullptr;
std::size_t *pUser = nullptr;
};
13.3 交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。对于那些与重排元素顺序的算法(如sort、unique)一起使用的类来说,定义swap是非常重要的,因为这类算法在需要交换两个元素时会调用swap。如果一个类定义了自己的swap,那么算法将使用类自定义的版本,否则,算法将使用标准库定义的swap(通常这会影响效率,有时甚至会产生错误的结果)。
通常我们知道常见的交换两个对象的方法是进行一次拷贝和两次赋值,标准库中的默认版本就采用此种方法:
//交换v1,v2
Hasptr temp = v1;
v1 = v2;
v2 = temp;
但是当一个类存在自己分配的资源时,这样重新分配资源是十分浪费的,比如对于HasPtr类来说,我们更希望交换指针,而不是在每次交换时都分配新的string副本。
编写自己的swap函数
可以在我们的类上定义一个自己版本的swap函数来重载swap的默认行为,如下:
class Has