拷贝控制和资源管理——行为像值的类、行为像指针的类

类的行文像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值得对象是,副本和原对象是完全独立的。改变副本对原对象有任何印象,反之亦然.
行为像指针的类则共享状态,当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副也会改变原对象,反之亦然

行为像值的类

假设一个HasPtr类,有两个成员,一个int和一个string指针。
为了提供类值得行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。这意味着对于ps指向的string,每个HasPtr对象都必须有自己的拷贝。为了实现类值得行为,HasPtr需要

  • 定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
  • 定义一个析构函数来释放string
  • 定义一个拷贝赋值运算符来释放对象当前的string,并从右侧运算对象拷贝string

类值版本的HasPtr如下所示

class HasPtr{
public:
	HasPtr(const std::string&s=std::string()):ps(new std::string(s)),i(0){}
	//对ps指向的string,每个HasPtr对象都有自己的拷贝
	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;
};

类的拷贝赋值运算

赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。但是,非常重要的一点是,这些操作是以正确的顺序执行的,即使将一个对象赋予它自身,也保证正确。而且,如果可能,我们编写的赋值运算符还应该是异常安全的——当异常发生时能将左侧运算对象置于一个有意义的状态
在本例中,通过先拷贝右侧运算对象,我们可以处理自赋值运算情况,并能保证在异常发生时代码也是安全的。在完成拷贝后,我们释放左侧运算对象的资源,并更新指针指向新分配的string

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
	auto newp=new string (*rhs.ps);//拷贝底层string
	delete ps;//释放就内存
	ps =newp;//从右侧运算对象拷贝数据到本对象;
	i=rhs.i;
	return *this;
}

在这个赋值运算符中,非常清楚,我们首先进行构造函数的工作:newp的初始化器等价于HasPtr的拷贝构造函数中ps的初始化器。接下来与析构函数一样,我们delete当前ps指向的string,然后就只剩下拷贝指向新分配的string的指针,以及从rhs拷贝Int值到本对象

定义行为像指针的类

对应行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是指向的string。我们的类仍然需要自己的析构函数来释放接受string参数的构造函数分配的内存。但是,在本例中,析构函数不能单方面地释放关联地string。只有当最后一个指向string的HasPtr销毁时,才可以释放string
令一个类展现类似指针的行为的最好方法是使用shared_ptr来管理类中的资源。拷贝(或赋值)一个shared_ptr会拷贝(赋值)shared_ptr所指向的指针。shared_ptr类自己记录有多少个用户共享它所指向的对象,当没有用户使用对象时,shared_ptr类负责释放资源。
但是,有时我们希望直接管理资源。在这种情况下,使用引用计数就很有用了。为了说明引用计数如何工作的,我们将重新定义HasPtr,令其行为像指针一样,但我们不使用shared_ptr,而是设计自己的引用计数

引用计数

引用计数的工作方式如下:

  • 除了初始化对象外,每个构造函数(拷贝构造函数出来)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我么创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1、
  • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又给一个新用户共享。
  • 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则关析构函数释放状态
  • 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态

唯一的难点是确定在哪里存放引用计数器。计数器不能直接作为HasPtr对象的成员。解决此问题的一种方法是将计数器保存在动态内存中。当创建一个对象时,我们也分配一个新的计数器。当拷贝或赋值对象时,我们拷贝指向计数器的指针。使用这种方法,副本和元对象都会指向相同的计数器。

定义一个使用引用计数的类

class HasPtr{
public:
//拷贝构造函数分配新的string和新的计数器,将计数器置位1
	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;//用来记录有多少个对象共享*ps的成员
}

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

当拷贝或赋值一个HasPtr对象时。我们希望副本原对象都指向相同的string,即,当拷贝一个HasPtr时,我们将拷贝ps本身,而不是ps指向的string。当我们进行拷贝时,还会递增该string关联的计数器。
(我们在类内定义的)拷贝构造函数拷贝给定的HasPtr的所有三个数据成员。这个构造函数还会递增use成员。指出ps和p.ps指向的string又有了一个新的用户。
析构函数不能无条件地delete ps——可能还会有其他对象指向这块内存,析构函数应该递减引用计数器,指出共享string的对象少了一个。如果计数器变为0,则析构函数释放ps和use指向的内存:

HasPtr::~HasPtr()
{
	if(--*use==0){//如果引用计数器变为0
	delete ps;//释放string 内存
	delete use;//释放计数器内存
	}
}

拷贝赋值运算符与往常一样执行类似拷贝构造函数和析构函数的工作。即,它必须递增右侧运算对象的引用计数,并递减左侧运算对象的引用计数,在必要时释放使用的内存(即,析构函数的工作)
而且与往常一样,赋值运算符必须支持处理自赋值。我们通常先递增rhs中的计数然后我们再递减左侧运算对象中的计数来实现这一点。通过这种方法,当两个对象相同时,再我们检查ps(即use)是否释放之前,计时器就已经被递增过了。

HasPtr& HasPtr::operator=(const HasPtr&rhs)
{
	++*rhs.use;//递增右侧原运算对象的引用计数
	if(--*use==0)//递减本对象的引用计数
	{			//如果没有其他用户,释放本对象分配的成员
		delete ps;
		delete  use;
	}
	ps=rhs.ps;//将数据从rhs拷贝到本对象
	i=rhs.i;
	use=rhs.use;
	return *this;//返回本对象
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值