C++Primer 拷贝控制和资源管理

类型对象的拷贝语义,一般来说,有两者选择:可以定义拷贝操作,使类的行为看起来像一个值或者一个指针。
类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本也会改变原对象,反之亦然。
行为像指针的类则共享状态。副本和原对象使用相同的底层数据,改变副本也会改变原对象。
在我们使用过的标准库类中,标准库容器和string类的行为像一个值。而shared_ptr类提供类似指针的行为。I/O类型和unique_ptr不允许拷贝或赋值,因此它们的行为既不像值也不像指针。

练习13.22:假定我们希望HasPtr的行为像一个值。即,对于对象所指向的string成员,每个对象都有一份自己拷贝。为HasPtr类编写拷贝构造函数和拷贝赋值运算符。

#include <iostream>
#include <string>
using namespace std;
class HasPtr {
public:
	HasPtr(const string &s = string()):ps(new string(s)),i(0){}   //默认构造函数
	HasPtr(const HasPtr& p) {        //拷贝构造函数
		ps = new string(*p.ps);
		i = p.i;
	}
	HasPtr& operator=(const HasPtr&);  //拷贝赋值运算符
	HasPtr& operator=(const string&);   //赋予新string
	string& operator*();               //解引用
	~HasPtr();    //析构函数
private:
	string* ps;
	int i;
};
HasPtr::~HasPtr() {
	delete ps;            //释放string的内存
}
inline HasPtr& HasPtr::operator=(const HasPtr &rhs) {
	auto newps = new string(*rhs.ps);     //拷贝指针指向的对象
	delete ps;           //销毁原string
	ps = newps;         //指向新的string
	i = rhs.i;       //使用内置的int赋值
	return *this;     //返回此对象的引用
}
HasPtr& HasPtr::operator=(const string& rhs) {
	*ps = rhs;
	return *this;
}
string& HasPtr::operator*() {
	return *ps;
}
int main() {
	HasPtr h("hi mom");
	HasPtr h2(h);  //行为类值,h2、h3和h指向不同的string
	HasPtr h3 = h;
	h2 = "hi dad";
	h3 = "hi son";
	cout << "h: " << *h << endl;
	cout << "h2: " << *h2 << endl;
	cout << "h3: " << *h3 << endl;
	return 0;
}

行为像值的类
为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。这意味着对于ps指向的string,每个HasPtr对象都必须有自己的拷贝,为了实现改拷贝,HasPtr需要:
1、定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
2、定义一个析构函数来释放string
3、定义一个拷贝赋值运算符来释放对象当前的string,并从右侧运算对象拷贝string

class HasPtr {
public:
	HasPtr(const string& s = string()) :ps(new string(s)), i(0) {}   //默认构造函数
	HasPtr(const HasPtr& p) {        //拷贝构造函数
		ps = new string(*p.ps);     //对于ps指向的string,每个HasPtr对象都有自己的拷贝
		i = p.i;
	}
	HasPtr& operator=(const HasPtr&);  //拷贝赋值运算符
	~HasPtr() { delete ps; }  //析构函数
private:
	std::string* ps;
	int i;
};

类值拷贝赋值运算符
赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。但是应该保证以正确的数据进行操作

HasPtr& HasPtr::operator=(const HasPtr &rhs) {
	auto newps = new string(*rhs.ps);     //拷贝底层string
	delete ps;           //销毁旧内存
	ps = newps;         //从右侧运算对象拷贝数据到本地
	i = rhs.i;       
	return *this;     //返回本对象
}

当编写赋值运算符时,有两点需要记住:
1、如果将一个对象赋予它自身,赋值运算符必须能正确工作
2、大多数赋值运算符组合了析构函数和拷贝构造函数的工作

当编写一个赋值运算符时,一个好的模式是将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后,销毁左侧运算对象的现有成员就是完全的。一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。

//编写错误的赋值运算符案例
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
	delete ps;     //销毁原string
	//如果rhs和*this是同一个对象,我们将从已释放的内存中拷贝数据
	auto newps = new string(*rhs.ps);     //拷贝指针指向的对象
	ps = newps;         //指向新的string
	i = rhs.i;       //使用内置的int赋值
	return *this;     //返回此对象的引用
}

所以,应该在销毁左侧运算对象资源之前拷贝右侧运算对象

练习13.24 如果在HasPtr版本中未定义析构函数和拷贝构造函数会发生什么?
如果未定义析构函数,在销毁HasPtr对象时合成的析构函数不会释放指针ps指向的内存,造成内存泄漏。
如果未定义拷贝构造函数,在拷贝HasPtr对象时,合成的拷贝构造函数会简单的复制ps的值,使得两个HasPtr指向相同的string。当其中一个HasPtr修改string内容时,另一个HasPtr中的string的内容也会被修改。当销毁其中一个HasPtr时,另一个HasPtr中的ps就会成为空悬指针

定义行为像指针的类
我们的类仍然需要析构函数来释放接受string参数的构造函数分配的内存。但是,本例中,析构函数不能单方面地释放关联的string。只有当最后一个指向string的HasPtr销毁后,才能释放string。

令一个类展现类似指针的行为的最好的办法是使用shared_ptr来管理类中的资源。shared_ptr类自己记录有多少用户共享它所指向的对象。当没有对象使用时,shared_ptr类负责释放资源。

当我们想直接管理资源时,可以使用引用计数

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

唯一的难题是在哪里存放引用计数器。计数器不能直接作为HasPtr对象的成员
下面例子说明原因:

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

如果计数器保存在每个对象中,当创建p2时,我们可以递增p1中的计数器并拷贝到p2,但当创建p3时,我们如果递增p1中的计数器拷贝到p3就是错误的。

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

class HasPtr {
public:
	//构造函数分配新的string和新的计数器,将计数器置为1
	HasPtr(const string& s = string()) :ps(new 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关联的计数器。
析构函数不能无条件地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;
	i = rhs.i;  
	use = rhs.use;     
	return *this;     //返回本对象
}

练习13.27: 定义使用引用计数版本的HasPtr

#include <iostream>
#include <string>
using namespace std;
class HasPtr {
public:
	//构造函数分配新的string和新的计数器,将计数器置为1
	HasPtr(const string& s = string()) :ps(new 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& operator=(const string& rhs);   //赋予新的string
	~HasPtr();      //析构函数
	string& operator*();               //解引用
private:
	std::string* ps;
	int i;
	std::size_t* use;  //用来记录有多少个对象共享*ps的成员
};
HasPtr::~HasPtr() {
	if (-- * use == 0) {   //如果计数器变为0
		delete ps;      //释放string内存
		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;     //返回本对象
}
HasPtr& HasPtr::operator=(const string& rhs) {
	*ps = rhs;
	return *this;
}
string& HasPtr::operator*() {
	return *ps;
}
int main() {
	HasPtr h("hi mom");
	HasPtr h2(h);  //行为类指针,h2、h3和h指向相同的string
	HasPtr h3 = h;
	h = "hi dad";    //改变h对象中指向string指针指向的值,则h2和h3也会改变为相同的值
	cout << "h: " << *h << endl;
	cout << "h2: " << *h2 << endl;
	cout << "h3: " << *h3 << endl;
	return 0;
}

练习13.28:给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员

class TreeNode{
private:
	std::string value;
	int count;
	TreeNode *left;
	TreeNode *right;
};
class BinStrNode{
private:
	TreeNode *root;
};

该结构为二叉树数据结构,实现类指针行为,且count用作引用计数
默认构造函数:

TreeNode::TreeNode() : value(""), count(1), left(nullptr), right(nullptr) {}
BinStrTree::BinStrTree() : root(nullptr) {}

拷贝构造函数:

BinStrTree::BinStrTree(const BinStrTree& bst) : root(bst.root) {   //拷贝整棵树
	root->CopyTree();   //应拷贝整棵树,而不是根节点
}
void TreeNode::CopyTree(void) {    //拷贝以此节点为根的子树,增加引用计数
	if (left) {
		left->CopyTree();   //左子树不空,拷贝左子树
	}
	if (right) {
		right->CopyTree();   //右子树不空,拷贝右子树
	}
	count++;
}
//从某个节点开始拷贝子树
TreeNode::TreeNode(const TreeNode &tn) : value(tn->value), count(1), left(tn->left), right(tn->right) {
	if (left) {
		left->CopyTree();   //左子树不空,拷贝左子树
	}
	if (right) {
		right->CopyTree();   //右子树不空,拷贝右子树
	}
}

析构函数:

BinStrTree::~BinStrTree() {   //释放整棵树
	if (!root->ReleaseTree()) {   //释放整棵树,而非仅仅根节点
		delete root;            //引用计数为0,释放节点空间
	}
}
int TreeNode::ReleaseTree() {       //释放以此节点为根的子树
	if (left) {
		if (!left->ReleaseTree()) {
			delete left;             //左孩子引用计数为0,释放其空间
		}
	}
	if (right) {
		if (!right->ReleaseTree()) {
			delete right;               
		}
	}
	count--;
	return count;
}
TreeNode::~TreeNode() {
	if (count)
		ReleaseTree();
}
发布了64 篇原创文章 · 获赞 4 · 访问量 908

猜你喜欢

转载自blog.csdn.net/CLZHIT/article/details/104030979