C++Primer 拷贝控制示例Message类和Folder类的实现

我们将概述两个类的设计,这两个类可能用于邮件处理应用中。两个类命名为Message和Folder,分别表示电子邮件消息和消息目录。

每个Message对象可以出现在多个Folder中。但是,任意给定的Message的内容只有一个副本。这样,如果一条Message的内容被改变,则我们从它所在的任何Folder来浏览此Message时,都会看到改变后的内容。

为了记录Message位于哪些Folder中,每个Message都会保存一个它所在Folder的指针的set,同样的,每个Folder都保存一个它包含的Message的指针的set。

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

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

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

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

拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要做的工作。这种情况下,公共的工作应该放在private的工具函数中完成。

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

#include <set>
using namespace std;
class Folder;
class Message {
	friend class Folder;
public:
	//folders被隐式初始化为空集合
	explicit Message(const string &str = ""):contents(str){ }
	//拷贝控制成员,用来管理指向本Message的指针
	Message(const Message&);     //拷贝构造函数
	Message& operator=(const Message&);    //拷贝赋值运算符
	~Message();          //析构函数
	//从给定Folder集合中添加/删除本Message
	void save(Folder&);
	void remove(Folder&);
private:
	string contents;     //实际消息文本
	set<Folder*> folders;     //包含Message的Folder
	//拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
	//将本Message添加到指向参数的Folder中
	void add_to_Folders(const Message&);
	//从folders中的每个Folder中删除Message
	void remove_from_Folders();
};

这个类定义了两个数据成员:contents,保存消息文本;folders,保存指向本Message所在Folder的指针。接受一个string参数的构造函数将给定string拷贝给contents,并将folders(隐式)初始化为空集。

save和remove成员
除拷贝控制成员外,Message类只有两个公共成员:save,将本Message存放在给定Folder中;remove,删除本Message:

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,需要更新本Message的folders成员。当save一个Message时,我们应保存一个指向给定Folder的指针。当remove一个Message时,我们需要删除此指针。
这些操作还必须更新给定的Folder。更新一个Folder的任务是由Folder类的addMsg和remMsg成员来完成的,分别添加和删除给定Message的指针。

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

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

此例中我们对m.folders中每个Folder调用addMsg。函数addMsg会将本Message的指针添加到每个Folder中。
Message的拷贝构造函数拷贝给定对象的数据成员:

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

并调用add_to_Folders将创建的Message的指针添加到每个包含原Message的Folder中。

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

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

析构函数如下:

Message::~Message() {
	remove_from_Folders();
}

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

Message的拷贝赋值运算符
Message类的拷贝赋值运算符必须执行拷贝构造函数和析构函数的工作。即使左侧和右侧运算对象是同一个Message,拷贝赋值运算符也能正确执行。在本例中,我们先从左侧运算对象的folders中删除此Message的指针,然后再将指针添加到右侧运算符对象的folders中,从而实现了自赋值的正确处理:

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;
}

如果左侧和右侧对象是相同的Message,则它们具有相同的地址。如果我们在add_to_Folders之后再调用remove_from_Folders,就会将此Message从它所在的所有Folder中删除。

Message的swap函数
标准库中定义了string和set的swap版本。因此,如果为我们的Message类定义它自己的swap版本,我们可以避免对contents和folders成员进行不必要的拷贝。
但是,我们的swap函数必须管理指向被交换Message的Folder指针。在调用swap(m1,m2)之后,原来指向m1的Folder现在必须指向m2,反之亦然。
我们通过两遍扫描folders中每个成员来正确处理Folder指针。第一遍扫描将Message从它们所在的Folder中删除。接下来我们调用swap来交换数据成员。最后,对于folders进行第二遍扫描来添加交换过的Message:

void Message::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, lhs.contents);     //使用swap(string&, string&)
	//将每个Message的指针添加到它的(新)Folder中
	for (auto f : lhs.folders)
		f->addMsg(&lhs);
	for (auto f : rhs.folders)
		f->addMsg(&rhs);
}

练习13.33:为什么Message的成员save和remove的参数是一个Folder&?为什么我们不将参数定义为Folder或是const Folder&?
首先,我们需要将给定Folder的指针添加到当前Message的folders集合中。这样,参数类型就不能是Folder,必须是引用类型。否则,调用save时会将实参拷贝给形参,folders.insert添加的就是形参(与局部变量一样在栈中,save执行完毕就被销毁)的指针,而非原始Folder指针。而参数为引用类型,就可以令形参和实参指向相同的对象,对形参f取地址,得到的就是原始的Folder(实参)的指针。
其次,我们需要调用addMsg将当前Message的指针添加到Folder的messages集合中,这意味着我们修改了Folder的内容,因此参数不能const的。

练习13.35:如果Message使用合成的拷贝控制成员,将会发生什么?
Message类包含两个数据成员:content为string类型,folders为set。这两个标准库类都有完备的拷贝控制成员,因此Message使用合法的拷贝控制成员的话,简单拷贝这两个成员也能实现正确拷贝。
但是,该问题需求不仅如此。当拷贝Message时,不仅要拷贝这个Message在哪些Folder中,还要将Message加到每个Folder中——调用addMsg。类似的,当销毁Message时,需要将它从所有Folder中删除——调用remMsg。因此,不能依赖合成的拷贝控制成员。

练习13.36:设计并实现对应的Folder类。此类应该保存一个指向Folder中包含的Message的set。

class Folder {
	friend class Message;
public:
	Folder(const Folder&);
	~Folder();
	Folder& operator=(const Folder&);
	void addMsg(Message* m) { msgs.insert(m); }
	void remMsg(Message* m) { msgs.erase(m); }
private:
	set<Message*> msgs;    //该Folder对象包含的Message信息
	void add_to_Messages(const Folder&);
	void remove_from_Msgs();
};
void Folder::add_to_Messages(const Folder& f) {
	for (auto msg : f.msgs) {
		msg->Message::addFldr(this);        //将这个Folder添加到所有Message中
	}
}
void Folder::remove_from_Msgs() {
	while (!msgs.empty())                   //将这个Folder从它所有Message中删除
		(*msgs.begin())->Message::remove(*this); 
}
Folder::Folder(const Folder& f) : msgs(f.msgs) {
	add_to_Messages(f);      //将Folder添加到它所有Message的folders中
}
Folder::~Folder() {
	remove_from_Msgs();
}
Folder& Folder::operator=(const Folder& f) {
	remove_from_Msgs();      //从每个Message中删除此Folder
	msgs = f.msgs;           //从右侧运算对象拷贝Message集合
	add_to_Messages(f);      //将此Folder添加到每个新Message中
	return *this;
}
class Message {
	friend class Folder;
public:
	//folders被隐式初始化为空集合
	explicit Message(const string &str = ""):contents(str){ }
	//拷贝控制成员,用来管理指向
	essage的指针
	Message(const Message&);     //拷贝构造函数
	Message& operator=(const Message&);    //拷贝赋值运算符
	~Message();          //析构函数
	//从给定Folder集合中添加/删除本Message
	void save(Folder&);
	void remove(Folder&);   
	void swap(Message&, Message&);
	void addFldr(Folder* f) { folders.insert(f); }    //添加给定的Folder
	void remFldr(Folder* f) { folders.erase(f); }     //删除给定的Folder
private:
	string contents;     //实际消息文本
	set<Folder*> folders;     //包含Message的Folder
	//拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
	//将本Message添加到指向参数的Folder中
	void add_to_Folders(const Message&);
	//从folders中的每个Folder中删除Message
	void remove_from_Folders();
};
void Message::save(Folder& f) {
	folders.insert(&f);    //将给定Folder添加到我们的Folder列表中     //将Folder从此Message中删除
	f.addMsg(this);        //将本Message添加到我们f的Message集合中    //反向,将Message也从Folder中删除
}
void Message::remove(Folder& f) {
	folders.erase(&f);     //将给定的Folder从我们的Folder列表中删除
	f.remMsg(this);        //将本Message从f的集合中删除
}
void Message::add_to_Folders(const Message& m) {
	for (auto f : m.folders)   //对每个包含m的Folder
		f->addMsg(this);       //向该Folder添加一个指向本Message的指针
} 
Message::Message(const Message& m) : contents(m.contents), folders(m.folders) {
	add_to_Folders(m);      //将本消息添加到指向m的Folder中
}
//从对应Folder中删除本Message
void Message::remove_from_Folders() {
	for (auto f : folders)     //对folders中每个指针
		f->remMsg(this);       //从该Folder中删除本Message
}
Message::~Message() {
	remove_from_Folders();
}
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;
}
void Message::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, lhs.contents);     //使用swap(string&, string&)
	//将每个Message的指针添加到它的(新)Folder中
	for (auto f : lhs.folders)
		f->addMsg(&lhs);
	for (auto f : rhs.folders)
		f->addMsg(&rhs);
}

练习13.37:为Message类添加成员,实现向folders添加或删除一个给定的Folder*。这两个成员类似Folder类的addMsg和remMsg操作。

	void addFldr(Folder* f) { folders.insert(f); }    //添加给定的Folder
	void remFldr(Folder* f) { folders.erase(f); }     //删除给定的Folder

练习13.38:我们并未使用拷贝并交换方式来设计Message的赋值运算符。你认为其原因是?
如果采用拷贝并交换方式,执行过程是这样子的:
1、由于赋值运算符的参数是Message类型,因此会将实参拷贝给形参rhs,这会触发拷贝构造函数,将实参的contents和folders拷贝给rhs,并调用add_to_Folders将rhs添加到folders的所有文件夹中。
2、随后赋值运算符调用swap交换*this和rhs,首先遍历两者的folders,将它们从自己的文件夹中删除;然后调用string和set的swap交换它们的contents和folders;最后,再遍历两者新的folders,将它们分别添加到自己的新文件夹中。
3、最后,赋值运算符结束,rhs被销毁,析构函数调用remove_from_Folders将rhs从自己的所有问价夹中删除。
显然,语义是正确的,达到了预期的目的。但效率低下,rhs创建、销毁并两次添加和删除是无意义的。形参rhs为引用类型就能避免这些冗余操作。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值