C++ primer学习笔记-13章-拷贝控制

拷贝控制和资源管理

通常,管理类外资源的类必须定义拷贝控制成员。这种类需要通过析构函数来释放对象所分配的资源。

为了定义这些成员,首先得确定此类型对象的拷贝语义,一般有两种选择:

  • 定义拷贝操作,使类的行为像一个值,如string类。当我们拷贝一个像值的对象时,副本和原对象是独立的。改变副本不会对原对象有任何影响,反之亦然。
  • 定义拷贝操作,使类的行为像一个指针,如shared_ptr类。行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变对象,反之亦然。

定义行为像值的类

为了提供类值的行为,对于类管理的资源,每个对象都要有自己的一份拷贝。这就意味着对于ps指向的string,每个HasPtr对象都必须有自己的拷贝。为了实现类值行为。HasPtr需要:

  1. 类的构造函数需要可能需要动态分配其成员的副本
  2. 类的拷贝赋值运算符相当于结合了构造函数和析构函数的操作,首先销毁左侧运算对象的资源,再从右侧运算符对象拷贝资源,注意顺序
  3. 由于有上述的顺序存在,所以我们必须保证这样的拷贝赋值运算符是正确的:首先将右侧运算对象拷贝到一个临时的对象中,再销毁左侧的运算对象的现有成员,之后将临时对象中的数据成员拷贝至左侧对象中(防范自赋值的情况发生—首先就销毁了自身的成员,再进行拷贝自身则会访问到已经释放的内存中)
	HasPtr& operator= (const HasPtr& p)
	{
		auto new_ps = new string(*p.ps);
		delete ps;
		ps = new_ps;
		return *this;
	}

为什么首先将右侧运算对象拷贝到一个临时的对象中,再销毁左侧的运算对象的现有成员

	HasPtr& operator= (const HasPtr& p)
	{
		delete ps;
		ps = new string(*p.ps);;
		return *this;
	}

如果p和this本对象是同一个对象,delete ps会释放*this和p指向的string,那么当我试图拷贝*p.ps时,就会访问一个指向无效内存的指针,其行为和结果是未定义的。

定义行为像指针的类

对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。类仍然需要析构函数来释放接受string参数的构造函数分配的内存。但是析构函数不能单方面地释放关联地string,只有当最后一个指向string地HasPtr销毁时,它才可以释放string。

引用计数

令一个类展现类似指针的行为地最好方法是使用shared_ptr来管理类中的资源。但是,我们有时希望直接管理内存。在这种情况下,使用引用计数就很有用了。

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

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

自定义实现引用计数版

class HasPtr
{
public:
	HasPtr()= default;//默认构造函数

	HasPtr(string p, int ii ):ps(new string(p)),i(ii),use(new std::size_t(1)) {};//默认构造函数
	//拷贝构造函数,完成string 指针指向内容的拷贝和i值的拷贝
	HasPtr(const HasPtr& p) :ps(new string(*p.ps)), i(p.i), use(p.use) { ++* use; }

	//拷贝赋值运算符
	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;
	}
	//析构函数
	~HasPtr() { if (-- * use ==0) { delete ps; delete use; } }
private:
	string* ps;
	int i;

	std::size_t* use;
};

使用shared_ptr


class HasPtr
{
public:
	HasPtr()= default;//默认构造函数

	HasPtr(string p, int ii ):ps(make_shared<string>(p)),i(ii) {};//默认构造函数
	//拷贝构造函数,完成string 指针指向内容的拷贝和i值的拷贝
	HasPtr(const HasPtr& p) :ps(p.ps), i(p.i) {  }

	//拷贝赋值运算符
	HasPtr& operator= (const HasPtr& rhs)
	{
		ps = rhs.ps;
		i = rhs.i;
		return *this;
	}
	//析构函数
	~HasPtr() {  }

private:
	shared_ptr<string> ps;
	int i;
};

计数器不能直接作为HasPtr对象的成员。下例说明:

HasPtr p1("Hiya!");
HasPtr p2(p1);   //p1和p2指向相同的string
HasPtr p3(p1);   //p1、p2和p3都指向相同的string

如果引用计数保存在每个对象中,当创建p3时如何正确更新它呢?可以递增p1的计数器并将其拷贝到p3中,但如何更新p2的计数器呢?

解决此问题的方法是将计数器保存到动态内存中。当创建一个对象时,我们也分配一个新的计数器。当拷贝或赋值对象时,我们拷贝指向计数器的指针。使用这种方法,副本和原对象都会指向相同的计数器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值