【C++基础】第七十二课:[拷贝控制]拷贝控制示例

拷贝控制示例

Posted by x-jeff on May 20, 2023

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.拷贝控制示例

虽然通常来说分配资源的类更需要拷贝控制,但资源管理并不是一个类需要定义自己的拷贝控制成员的唯一原因。一些类也需要拷贝控制成员的帮助来进行簿记工作或其他操作。

作为类需要拷贝控制来进行簿记操作的例子,我们将概述两个类的设计,这两个类可能用于邮件处理应用中。两个类命名为Message和Folder,分别表示电子邮件(或者其他类型的)消息和消息目录。每个Message对象可以出现在多个Folder中。但是,任意给定的Message的内容只有一个副本。这样,如果一条Message的内容被改变,则我们从它所在的任何Folder来浏览此Message时,都会看到改变后的内容。

为了记录Message位于哪些Folder中,每个Message都会保存一个它所在Folder的指针的set,同样的,每个Folder都保存一个它包含的Message的指针的set。图13.1说明了这种设计思路。

我们的Message类会提供save和remove操作,来向一个给定Folder添加一条Message或是从中删除一条Message。为了创建一个新的Message,我们会指明消息内容,但不会指出Folder。为了将一条Message放到一个特定Folder中,我们必须调用save。

当我们拷贝一个Message时,副本和原对象将是不同的Message对象,但两个Message都出现在相同的Folder中。因此,拷贝Message的操作包括消息内容和Folder指针set的拷贝。而且,我们必须在每个包含此消息的Folder中都添加一个指向新创建的Message指针。

当我们销毁一个Message时,它将不复存在。因此,我们必须从包含此消息的所有Folder中删除指向此Message的指针。

当我们将一个Message对象赋予另一个Message对象时,左侧Message的内容会被右侧Message的内容所替代。我们还必须更新Folder集合,从原来包含左侧Message的Folder中将它删除,并将它添加到包含右侧Message的Folder中。

观察这些操作,我们可以看到,析构函数和拷贝赋值运算符都必须从包含一条Message的所有Folder中删除它。类似的,拷贝构造函数和拷贝赋值运算符都要将一个Message添加到给定的一组Folder中。我们将定义两个private的工具函数来完成这些工作。

Folder类也需要类似的拷贝控制成员,来添加或删除它保存的Message。我们将假定Folder类包含名为addMsg和remMsg的成员,分别完成在给定Folder对象的消息集合中添加和删除Message的工作。

1.1.Message类

根据上述设计,我们可以编写Message类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Message {
	friend class Folder;
public:
	//folders被隐式初始化为空集合
	explicit Message(const std::string &str = "") : contents(str) { }
	//拷贝控制成员,用来管理指向本Message的指针
	Message(const Message&); //拷贝构造函数
	Message& operator=(const Message&); //拷贝赋值运算符
	~Message();
	//从给定Folder集合中添加/删除本Message
	void save(Folder&);
	void remove(Folder&);
private:
	std::string contents; //实际消息文本
	std::set<Folder*> folders; //包含本Message的Folder
	//拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
	//将本Message添加到指向参数的Folder中
	void add_to_Folders(const Message&);
	//从folders中的每个Folder中删除本Message
	void remove_from_Folders();
};

1.2.save和remove成员

1
2
3
4
5
6
7
8
9
10
void Message::save(Folder &f)
{
	folders.insert(&f); //将给定Folder添加到我们的Folder列表中
	f.addMsg(this); //将本Message添加到f的Message集合中
}
void Message::remove(Folder &f)
{
	folders.erase(&f); //将给定Folder从我们的Folder列表中删除
	f.remMsg(this); //将本Message从f的Message集合中删除
}

1.3.Message类的拷贝控制成员

当我们拷贝一个Message时,得到的副本应该与原Message出现在相同的Folder中。因此,我们必须遍历Folder指针的set,对每个指向原Message的Folder添加一个指向新Message的指针。拷贝构造函数和拷贝赋值运算符都需要做这个工作,因此我们定义一个函数来完成这个公共操作:

1
2
3
4
5
6
//将本Message添加到指向m的Folder中
void Message::add_to_Folders(const Message &m)
{
	for(auto f : m.folders) //对每个包含m的Folder
		f->addMsg(this); //向该Folder添加一个指向本Message的指针
}

Message的拷贝构造函数拷贝给定对象的数据成员:

1
2
3
4
Message::Message(const Message &m) : contents(m.contents), folders(m.folders)
{
	add_to_Folders(m); //将本消息添加到指向m的Folder中
}

1.4.Message的析构函数

当一个Message被销毁时,我们必须从指向此Message的Folder中删除它。拷贝赋值运算符也要执行此操作,因此我们会定义一个公共函数来完成此工作:

1
2
3
4
5
6
//从对应的Folder中删除本Message
void Message::remove_from_Folders()
{
	for(auto f : folders) //对folders中每个指针
		f->remMsg(this); //从该Folder中删除本Message
}

编写析构函数:

1
2
3
4
Message::~Message()
{
	remove_from_Folders();
}

调用remove_from_Folders确保没有任何Folder保存正在销毁的Message的指针。编译器自动调用string的析构函数来释放contents,并自动调用set的析构函数来清理集合成员使用的内存。

1.5.Message的拷贝赋值运算符

与大多数赋值运算符相同,我们的Message类的拷贝赋值运算符必须执行拷贝构造函数和析构函数的工作。与往常一样,最重要的是我们要组织好代码结构,使得即使左侧和右侧运算对象是同一个Message,拷贝赋值运算符也能正确执行。

在本例中,我们先从左侧运算对象的folders中删除此Message的指针,然后再将指针添加到右侧运算对象的folders中,从而实现了自赋值的正确处理:

1
2
3
4
5
6
7
8
9
Message& Message::operator=(const Message &rhs)
{
	//通过先删除指针再插入它们来处理自赋值情况
	remove_from_Folders(); //更新已有Folder
	contents = rhs.contents; //从rhs拷贝消息内容
	folders = rhs.folders; //从rhs拷贝Folder指针
	add_to_Folders(rhs); //将本Message添加到那些Folder中
	return *this;
}

1.6.Message的swap函数

标准库中定义了string和set的swap版本。因此,如果为我们的Message类定义它自己的swap版本,它将从中受益。通过定义一个Message特定版本的swap,我们可以避免对contents和folders成员进行不必要的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void swap(Message &lhs, Message &rhs)
{
	using std::swap; //在本例中严格来说并不需要,但这是一个好习惯
	//将每个消息的指针从它(原来)所在Folder中删除
	for(auto f : lhs.folders)
		f->remMsg(&lhs);
	for(auto f : rhs.folders)
		f->remMsg(&rhs);
	//交换contents和Folder指针set
	swap(lhs.folders, rhs.folders); //使用swap(set&, set&)
	swap(lhs.contents, rhs.contents); //swap(string&, string&)
	//将每个Message的指针添加到它的(新)Folder中
	for(auto f : lhs.folders)
		f->addMsg(&lhs);
	for(auto f : rhs.folders)
		f->addMsg(&rhs);
}