C++Primer学习(13.4 拷贝控制示例)

13.4 拷贝控制示例

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

作为类需要拷贝控制来进行簿记操作的例子,我们将概述两个类的设计,这两个类可能用于邮件处理应用中。两个类命名为Message和Folder,分别表示电子邮件(或者其他类型的)消息和消息目录。每个Message对象可以出现在多个Folder中。但是,任意给定的Message的内容只有一个副本。这样,如果一条essage的内容被改变则我们从它所在的任何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的所有Foder中删除它。类似的,拷贝构造函数和拷贝赋值运算符都要将一个Message 添加到给定的一组Folder中。我们将定义两个 private 的工具函数来完成这些工作。

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

Folder类也需要类似的拷贝控制成员,来添加或删除它保存的Message。

我们将Folder类的设计和实现留作练习。但是,我们将假定Folder类包含名为addMsg和remMsg的成员,分别完成在给定Folder对象的消息集合中添加和删除Message的工作。
Message 类

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

cpp 复制代码
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_t_Folders(const Message&);
	//从folders中的每个Folder中删除本Message
	void remove_from_Folders();
};

这个类定义了两个数据成员:contents,保存消息文本;folders,保存指向本 Message所在Folder的指针 。接受一个string参数的构造函数将给定string拷贝给contents,并将folders(隐式)初始化为空集。由于此构造函数有一个默认参数,因此它也被当作Message的默认构造函数(参见7.5.1节,第260页)。
save和remove 成员

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

cpp 复制代码
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,需要更新本Message的folders成员。当save一个Message 时,我们应保存一个指向给定Folder的指针;当remove一个Message时,我们要删除此指针。

这些操作还必须更新给定的Folder。更新一个Folder的任务是由Folder 类的addMsg和remMsg成员来完成的,分别添加和删除给定message的指针。
Message 类的拷贝控制成员

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

cpp 复制代码
//将本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的拷贝构造函数拷贝给定对象的数据成员:

cpp 复制代码
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 中删除它。拷贝赋值运算符也要执行此操作,因此我们会定义一个公共函数来完成此工作:

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

函数remove_from_Folders的实现类似 add_to_Folders,不同之处是它调用remMsq来删除当前Message而不是调用addMsg来添加Message。有了remove_from_Folders函数,编写析构函数就很简单了:

cpp 复制代码
Message::~Message()
{
	remove_from_Folders();
}

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

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

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

cpp 复制代码
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版本(参见9.2.5节,第 303页)。因此,如果为我们的 Message 类定义它自己的swap 版本,它将从中受益。通过定义一个Message 特定版本的 swap,我们可以避免对 contents 和 folders 成员进行不必要的拷贝。但是,我们的swap函数必须管理指向被交换Message的Folder指针。在调用swap(m1,m2)之后,原来指向m1的Folder 现在必须指向 m2,反之亦然。

我们通过两遍扫描folders中每个成员来正确处理Folder指针。第一遍扫描将Message从它们所在的Folder中删除。接下来我们调用swap 来交换数据成员。最后对 folders进行第二遍扫描来添加交换过的Message:

cpp 复制代码
void swap(Message &lhs,Message &rhs)
{
	using std::swap;//在本例中严格来说并不需要,但这是一个好习惯
	//将每个消息的指针从它(原来)所在Folder中删除
	for(auto f:lhs.folders)
	f->remMsq(&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->addMsq(&lhs);
	for(auto f:rhs.folders)
	f->addMsg(&rhs);
}

知识补充:基于范围的for循环:

for(auto f: m.folders)

这行代码使用了 C++11 引入的基于范围的 for 循环(Range-based for loop),用于遍历 m.folders 容器中的每个元素。
1. 语法结构

基于范围的 for 循环的基本语法如下:

cpp 复制代码
for (declaration : range_expression) 
{
    // 循环体
}

declaration:用于声明一个变量,该变量会在每次循环迭代时依次绑定到 range_expression 所表示的序列中的每个元素。

range_expression:表示一个可迭代的对象,如数组、std::vector、std::list 等容器。
2. 代码分析

在 for(auto f: m.folders) 中:

auto:这是 C++ 的类型推导关键字,编译器会根据 m.folders 容器中元素的实际类型自动推导出 f 的类型。这样可以避免手动指定类型,使代码更加简洁。

f:是一个临时变量,在每次循环迭代时,它会依次绑定到 m.folders 容器中的每个元素。

m.folders:是一个可迭代的容器对象,例如 std::vector、std::list 等,它包含了一系列元素。

  1. 示例代码
cpp 复制代码
 #include <iostream>
#include <vector>
struct MyStruct 
{
    std::vector<int> folders;
};
int main() 
{
    MyStruct m;
    m.folders = {1, 2, 3, 4, 5};
    //等效于下面的操作:
   // m.folders.push_back(1);
   // m.folders.push_back(2);
   // m.folders.push_back(3);
   // m.folders.push_back(4);
   // m.folders.push_back(5);
    for (auto f : m.folders) 
    {
        std::cout << f << " ";
    }
    //等效于下面的操作:
    // 使用迭代器遍历 m.folders
   // for (std::vector<int>::iterator it = m.folders.begin(); it != m.folders.end(); ++it) 
   //{
   //     std::cout << *it << " ";
   // }
    std::cout << std::endl;
    std::cout << std::endl;
    return 0;
}

代码解释

在 main 函数中,定义了一个 MyStruct 类型的对象 m,并为其 folders 成员(一个 std::vector 容器)初始化了一些整数元素。

使用基于范围的 for 循环遍历 m.folders 容器,将每个元素依次赋值给变量 f,并将其输出到控制台。
注意事项

如果需要修改容器中的元素,应该使用引用类型,例如 for (auto& f : m.folders)。

如果不希望修改元素,但想要避免不必要的拷贝,可以使用 const 引用类型,例如 for (const auto& f : m.folders)。

相关推荐
xinxiyinhe17 分钟前
GitHub上免费学习工具的精选汇总
学习·github
m0_687399841 小时前
build cinecert/asdcplib to .so or .a
c++·ubuntu·dci
大草原的小灰灰1 小时前
C/C++回调函数实现与std::function和std::bind介绍
c语言·c++
傍晚冰川1 小时前
【STM32】WDG看门狗(学习笔记)
c语言·笔记·科技·stm32·单片机·嵌入式硬件·学习
Moonnnn.1 小时前
运算放大器(三)运算放大器的典型应用
笔记·学习·硬件工程
大锦终2 小时前
详解list容器
c语言·开发语言·数据结构·c++·list
DA02212 小时前
C++轻量HeaderOnly的JSON库
开发语言·c++·json
技术小齐2 小时前
网络运维学习笔记(DeepSeek优化版)026 OSPF vlink(Virtual Link,虚链路)配置详解
运维·网络·学习
热爱嵌入式的小许2 小时前
基于Linux C++多线程服务器 + Qt上位机开发 + STM32 + 8266WIFI的智慧无人超市
开发语言·c++·qt项目·linux项目·linux无人智慧超市·linux多线程服务器·单片机项目
weixin_445054722 小时前
力扣刷题-热题100题-第26题(c++、python)
c++·python·算法·leetcode