类的行为可以像一个值或指针:
类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值得对象时,副本与原对象是独立的,改变副本不会对原对象有任何影响,反之亦然。
类的行为像指针,将共享状态,拷贝这样的一个类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。
行为像值的类
提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。
class Hasptr{
public:
Hasptr(const std::string &s=std::string()):ps(new std::string(s)),i(0){}
Hasptr(const Hasptr &p):ps(new std::string(*p.ps)),i(p.i){}
Hasptr& operator=(const Hasptr&);
~Hasptr(){delete ps;}
private:
std::string *ps;
int i;
}
类值拷贝赋值运算符
赋值运算符组合了析构函数和构造函数的操作:
- 类似析构函数,赋值操作会销毁左侧运算对象的值;
- 类似拷贝构造函数,赋值操作会从右侧对象拷贝数据。
需要注意的是:
- 如果将一个对象赋予自身,赋值运算符必须能正常工作;
- 异常安全,即当异常发生时,能将左侧运算对象置于一个有意义的状态。
Hasptr& Hasptr::operator = (const Hasptr &rhs)
{
auto newp = new string(*rhs.ps); //拷贝底层的string
delete ps; //释放旧内存
ps = newp; //右侧对象拷贝数据到本对象
i = rhs.i;
return *this; //返回本对象
}
行为像指针的类
引用计数
引用计数的工作方式如下:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要构建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当创建一个对象时,只有一个对象共享状态,将此计数器初始化为1。
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新的用户所共享。
- 析构函数递减计数器,指出共享状态的用户少了一个,如果计数器为0,则析构函数释放状态。
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
计数器不能是 Hasptr 对象的成员:
Hasptr p1("Hiya");
Hasptr p2(p1);
Hasptr p3(p1); //p1,p2,p3指向相同的string
如果引用计数保存在每个对象中,当创建 p3
时,可以递增 p1
中的计数器并将其拷贝到 p3
,但是 p2
中的计数器无法更新。
解决办法是将计数器保存在动态内存中,当创建一个对象时,也分配一个新的计数器,当拷贝或赋值对象时,拷贝指向计数器的指针,使用此方法可以保证副本和原对象指向相同的计数器。
定义一个使用引用计数的类
class Hasptr
{
public:
Hasptr(const std::string &s = std::string()):ps(new std::string(s),i(0),use(new std::size_t(1))){}
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 *use;
}
析构函数需要检查引用计数是否为0:
Hasptr ::~Hasptr()
{
if(--*use == 0){
delete ps;
delete use;
}
}
拷贝赋值运算符:
Hasptr& Hasptr::operator=(const Hasptr& rhs)
{
++*rhs.use; //递增右侧对象的引用计数
if(--*use == 0) //递减和检测本对象的引用计数
{
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}