13.2:拷贝控制和资源管理
两种选择:
- 类的行为像一个值:有自己的状态,拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会改变原对象。
- 类的行为像一个指针:类是共享状态,当拷贝这个对象时,原对象和副本对象使用相同的底层数据,改变副本也会改变原对象。
1.行为像值的类
拷贝对象,而不是拷贝指针。
代码如下:
class HasPtr{
public:
HasPtr(const string &s = string()):ps(new string(s)), i(0) {}
//对ps指向的string,每个HasPtr对象都有自己的拷贝
HasPtr(const HasPtr &p):ps(new string(*p.ps)), i(p.i) {}
HasPtr& operator = (const HasPtr&);
~HasPtr() { delete ps; }
private:
string *ps;
int i;
};
HaPtr& HasPtr::operator = (const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); //拷贝底层string
delete ps; //释放旧内存
ps = newp; //从右侧对象拷贝数据到本对象
i = rhs.i;
return *this; //返回本对象。
}
注意要防范自赋值操作的错误。
错误代码:
HaPtr& HasPtr::operator = (const HasPtr &rhs)
{
delete ps; //释放对象指向的string
//但是如果rhs和*this是同一对象,就将从已经释放的内存中拷贝数据
ps = new string(*(rhs.ps));
i = rhs.i;
return *this; //返回本对象。
}
2.行为像指针的类
需要拷贝指针,而不是拷贝指针所指的对象。
对于共享资源,最好的方法是使用shared_ptr。当时如果希望直接管理资源,最好使用引用计数(使用自定义而非shared_ptr)。
工作方式如下:
- 在构造函数中,除了初始化对象外,还要创建一个引用计数,来记录共享状态。初始化时只有一个对象,引用计数初始化为1.
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。并且要递增共享的计数器。
- 析构函数递减计数器,当计数器变为0时,析构函数就可以释放成员。
- 拷贝赋值运算符要递增右侧运算对象的计数器,递减左侧对象的计数器。如果左侧运算对象计数器为0,拷贝赋值运算符就必须销毁此对象的状态。
为了更新引用计数,将计数器保存在动态内存中,当创建对象,分配一个新的计数器,拷贝或赋值对象时,拷贝指向计数器的指针。
代码如下:
class HasPtr{
public:
//构造函数分配新的string和新的计数器1
HasPtr(const string &s = string()):ps(new string(s)),i(0),use(new size_t(1)){}
//拷贝构造函数拷贝三个成员,并递增计数器
HasPtr(const HasPtr &p):ps(p.ps), i(p.i), use(p.use) { ++*use; }
HasPtr& operator = (const HasPtr&);
~HasPtr();
private:
string *ps;
int i;
size_t *use; //计数器,记录共享*ps的成员
};
HasPtr& HasPtr::operator = (const HasPtr &rhs)
{
++*rhs.use; //递增右侧运算对象的引用计数
--*use; //递减本对象的引用计数
if(*use == 0)
{
delete ps; //释放对象
delete use; //释放计数器
}
//拷贝rhs对象
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
HasPtr::~HasPtr()
{
--*use; //递减对象计数器
if(*use == 0)
{
delete ps; //释放对象
delete use; //释放计数器
}
}