《C++Primer》第十三章-复制控制-学习笔记(2)-析构函数

本文深入探讨C++中复制控制的概念,包括析构函数、复制构造函数和赋值操作符的使用。通过Message和Folder类实例,详细解析了如何在类设计中实现资源管理和对象生命周期控制。

《C++Primer》第十三章-复制控制-学习笔记(2)

日志:
1,2020-03-16 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

析构函数

构造函数(destructor)的一个用途是自动获取资源。 例如,构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充

何时调用析构函数

撤销类对象时会自动调用析构函数:

// p points to default constructed object
Sales_item *p = new Sales_item;
{
// new scope
Sales_item item(*p); // copy constructor copies *p into item
delete p; // destructor called on object pointed to by p
} // exit local scope; destructor called on item

变量(如 item)在超出作用域时应该自动撤销。 因此,当遇到右花括号时,将运行 item 的析构函数。
动态分配的对象只有在指向该对象的指针被删除时才撤销。 如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。

当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。

撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数:

{
Sales_item *p = new Sales_item[10]; // dynamically allocated
vector<Sales_item> vec(p, p + 10); // local object
// ...
delete [] p; // array is freed; destructor run on each element
} // vec goes out of scope; destructor run on each element  vec超出作用域撤销了;

容器中的元素总是按逆序撤销:首先撤销下标为 size() - 1 的元素,然后是下标为 size() - 2 的元素……直到最后撤销下标为 [0] 的元素

何时编写显式析构函数

许多类不需要显式析构函数,尤其是具有构造函数的类不一定需要定义自己的析构函数。仅在有些工作需要析构函数完成时,才需要析构函数。 析构函数通常用于释放在构造函数或在对象生命期内获取的资源。
如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。 这个规则常称为三法则(Rule of Three),指的是如果需要析构函数,则需要所有这三个复制控制成员。
析构函数并不仅限于用来释放资源。一般而言,析构函数可以执行任意操作,该操作是类设计者希望在该类对象的使用完毕之后执行的。

合成析构函数

与复制构造函数或赋值操作符不同编译器总是会为我们合成一个析构函数合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。

撤销内置类型成员或复合类型的成员没什么影响。尤其是,合成析构函数并不删除指针成员所指向的对象。

如何编写析构函数

Sales_item 类是类没有分配资源因此不需要自己的析构函数的一个例子。分配了资源的类一般需要定义析构函数以释放那些资源。
析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。

析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行
例如,可以为 Sales_item类编写如下的空析构函数:

class Sales_item {
public:
// empty; no work to do other than destroying the members,
// which happens automatically
~Sales_item() { }
// other members as before
};

撤销 Sales_item 类型的对象时,将运行这个什么也不做的析构函数,它执行完毕后,将运行合成析构函数(synthesized destructor)以撤销类的成员。合成析构函数调用 string 析构函数来撤销 string 成员,string 析构函数释放了保存 isbn 的内存。units_sold 和 revenue 成员是内置类型,所以合成析构函数撤销它们不需要做什么。

消息处理示例

有些类为了做一些工作需要对复制进行控制。 为了给出这样的例子,我们将概略定义两个类,这两个类可用于邮件处理应用程序。Message 类和 Folder 类分别表示电子邮件(或其他)消息和消息所出现的目录,一个给定消息可以出现在多个目录中。Message 上有 save 和 remove 操作,用于在指定 Folder 中保存或删除该消息。

对每个 Message,我们并不是在每个 Folder 中都存放一个副本,而是使每个 Message 保存一个指针集(set),set 中的指针指向该 Message 所在的Folder。每个 Folder 也保存着一些指针,指向它所包含的 Message。将要实现的数据结构如图 13.1 所示。
在这里插入图片描述

图 13.1. Message 和 Folder 类设计

创建新的 Message 时,将指定消息的内容但不指定 Folder。调用 save 将Message 放入一个 Folder。
复制一个 Message 对象时,将复制原始消息的内容和 Folder 指针集,还必须给指向源 Message 的每个 Folder 增加一个指向该 Message 的指针。
将一个 Message 对象赋值给另一个,类似于复制一个 Message:赋值之后,内容和 Folder 集将是相同的。首先从左边 Message 在赋值之前所处的 Folder中删除该 Message。原来的 Message 去掉之后,再将右边操作数的内容和Folders 集复制到左边,还必须在这个 Folder 集中的每个 Folders 中增加一个指向左边 Message 的指针。
撤销一个 Message 对象时,必须更新指向该 Message 的每个 Folder。一旦去掉了 Message,指向该 Message 的指针将失效,所以必须从该 Message 的Folder 指针集的每个 Folder 中删除这个指针。
查看这个操作列表,可以看到,析构函数和赋值操作符分担了从保存给定Message 的 Folder 列表中删除消息的工作。类似地,复制构造函数和赋值操作符分担将一个 Message 加到给定 Folder 列表的工作。我们将定义一对private 实用函数完成这些任务。

Message 类
对于以上的设计,可以如下编写 Message 类的部分代码:

class Message {
public:
// folders is initialized to the empty set automatically
	Message(const std::string &str = ""):contents (str) { }
// copy control: we must manage pointers to this Message
// from the Folders pointed to by folders
	Message(const Message&);
	Message& operator=(const Message&);
	~Message();
// add/remove this Message from specified Folder's set of messages
	void save (Folder&);
	void remove(Folder&);
private:
	std::string contents; // actual message text
	std::set<Folder*> folders; // Folders that have this Message
// Utility functions used by copy constructor, assignment, and destructor:
// Add this Message to the Folders that point to the parameter
	void put_Msg_in_Folders(const std::set<Folder*>&);
// remove this Message from every Folder in folders
	void remove_Msg_from_Folders();
};

Message 类定义了两个数据成员:contents 是一个保存实际消息的string,folders 是一个 set,包含指向该 Message 所在的 Folder 的指针。
构造函数接受单个 string 形参,表示消息的内容。构造函数将消息的副本保存在 contents 中,并(隐式)将 Folder 的 set 初始化为空集。这个构造函数提供一个默认实参(为空串),所以它也可以作为默认构造函数。
实用函数提供由复制控制成员共享的行为。put_Msg_in_Folders 函数将自身 Message 的一个副本添加到指向给定 Message 的各 Folder 中,这个函数执行完后,形参指向的每个 Folder 也将指向这个 Message。复制构造函数和赋值操作符都将使用这个函数。
remove_Msg_from_Folders 函数用于赋值操作符和析构函数,它从 folders成员的每个 Folder 中删除指向这个 Message 的指针。

Message 类的复制控制

复制 Message 时,必须将新创建的 Message 添加到保存原 Message 的每个 Folder 中。这个工作超出了合成构造函数的能力范围,所以我们必须定义自己的复制构造函数

Message::Message(const Message &m):
contents(m.contents), folders(m.folders)
{
// add this Message to each Folder that points to m
put_Msg_in_Folders(folders);
}

复制构造函数将用旧对象成员的副本初始化新对象的数据成员。除了这些初始化之外(合成复制构造函数可以完成这些初始化),还必须用 folders 进行迭代,将这个新的 Message 加到那个集的每个 Folder 中。复制构造函数使用put_Msg_in_Folder 函数完成这个工作。
编写自己的复制构造函数时,必须显式复制需要复制的任意成员。显式定义的复制构造函数不会进行任何自动复制。
像其他任何构造函数一样,如果没有初始化某个类成员,则那个成员用该成员的默认构造函数初始化。复制构造函数中的默认初始化不会使用成员的复制构造函数。

put_Msg_in_Folders 成员

put_Msg_in_Folders 通过形参 rhs 的成员 folders 中的指针进行迭代。
这些指针表示指向 rhs 的每个 Folder,需要将指向这个 Message 的指针加到每个 Folder。
函数通过 rhs.folders 进行循环,调用命名为 addMsg 的 Folder 成员来完成这个工作,addMsg 函数将指向该 Message 的指针加到 Folder 中。

// add this Message to Folders that point to rhs
void Message::put_Msg_in_Folders(const set<Folder*> &rhs)
{
for(std::set<Folder*>::const_iterator beg = rhs.begin();
beg != rhs.end(); ++beg)
(*beg)->addMsg(this); // *beg points to a Folder
}

这个函数中唯一复杂的部分是对 addMsg 的调用:

(*beg)->addMsg(this); // *beg points to a Folder

那个调用以 (*beg) 开关,它解除迭代器引用。解除迭代器引用将获得一个指向 Folder 的指针。然后表达式对 Folder 指针应用箭头操作符以执行addMsg 操作,将 this 传给 addMsg,该指针指向我们想要添加到 Folder 中的Message。

Message 赋值操作符

赋值比复制构造函数更复杂。像复制构造函数一样,赋值必须对 contents赋值并更新folders 使之与右操作数的 folders 相匹配。它还必须将该Message 加到指向 rhs 的每个 Folder 中,可以使用 put_Msg_in_Folders 函数完成赋值的这一部分工作。
在从 rhs 复制之前,必须首先从当前指向该 Message 的每个 Folder 中删除它。我们需要通过 folders 进行迭代,从 folders 的每个 Folder 中删除指向该 Message 的指针。命名为 remove_Msg_from_Folders 的函数将完成这项工作。
对于完成实际工作的 remove_Msg_from_Folders 和 put_Msg_in_Folders,赋值操作符本身相当简单:

Message& Message::operator=(const Message &rhs)
{
if (&rhs != this) {
remove_Msg_from_Folders(); // update existing Folders
contents = rhs.contents; // copy contents from rhs
folders = rhs.folders; // copy Folder pointers from rhs
// add this Message to each Folder in rhs
put_Msg_in_Folders(rhs.folders);
}
return *this;
}

赋值操作符首先检查左右操作数是否相同。查看函数的后续部分可以清楚地看到进行这一检查的原因。假定操作数是不同对象,调用remove_Msg_from_Folders 从 folders 成员的每个 Folder 中删除该Message。一旦这项工作完成,必须将右操作数的 contents 和 folders 成员赋值给这个对象。最后,调用 put_Msg_in_Folders 将指向这个 Message 的指针添加至指向 rhs 的每个 Folder 中。
了解了 remove_Msg_from_Folders 的工作之后,我们来看看为什么赋值操作符首先要检查对象是否不同。赋值时需删除左操作数,并在撤销左操作数的成员之后,将右操作数的成员赋值给左操作数的相应成员。如果对象是相同的,则撤销左操作数的成员也将撤销右操作数的成员!
即使对象赋值给自己,赋值操作符的正确工作也非常重要。保证这个行为的通用方法是显式检查对自身的赋值。

remove_Msg_from_Folders 成员

除了调用 remMsg 从 folders 指向的每个 Folder 中删除这个 Message之外,remove_Msg_from_Folders 函数的实现与 put_Msg_in_Folders 类似:

// remove this Message from corresponding Folders
void Message::remove_Msg_from_Folders()
{
// remove this message from corresponding folders
for(std::set<Folder*>::const_iterator beg =
folders.begin (); beg != folders.end (); ++beg)
(*beg)->remMsg(this); // *beg points to a Folder
}
Message 析构函数

剩下必须实现的复制控制函数是析构函数:

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

有了 remove_Msg_from_Folders 函数,编写析构函数将非常简单。我们调用 remove_Msg_from_Folders 函数清除 folders,系统自动调用 string 析构函数释放 contents,自动调用 set 析构函数清除用于保存 folders 成员的内存,因此,Message 析构函数唯一要做的是调用 remove_Msg_from_Folders。
赋值操作符通常要做复制构造函数和析构函数也要完成的工作。在这种情况下,通用工作应在 private 实用函数中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值