参考资料:
通常,管理类外资源的类必须定义拷贝控制成员。
为了定义这些成员,我们必须确定此类型对象的拷贝语义。一般来说,有两种选择:使类像一个值或像一个指针。
类的行为像一个值,意味着当我们拷贝一个值时,副本和对象是完全独立的,改变副本对原对象不会有任何影响,反之亦然;类的行为像一个指针,意味着副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。
为了说明这两种方式,我们将定义 HasPtr
类,该类拥有两个成员:一个 int
和一个 string
指针。
为了提供类值的行为,对于类管理的资源,每个对象都应该有自己的一份拷贝,为此,HasPtr
需要:
string
的拷贝,而不是拷贝指针;string
;string
,并从右侧运算对象拷贝 string
。class HasPtr {
public:
HasPtr(const string &s = string()):
ps(new string(s)), i(0){}
HasPtr(const HasPtr& h):
ps(new string(*(h.ps))), i(h.i){}
HasPtr &operator=(const HasPtr &rhs);
~HasPtr() { delete ps; }
private:
string *ps;
int i;
};
赋值运算符通常组合了析构函数和构造函数的操作:类似析构函数,赋值操作会销毁左侧运算对象的资源;类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
我们要合理安排操作的顺序,保证将一个对象赋予它自身也是安全的,并当异常发生时,将左侧运算对象置于一个有意义的状态:
HasPtr &HasPtr::operator=(const HasPtr &rhs) {
auto newp = new string(*(rhs.ps)); // 先拷贝底层string
delete ps; // 再释放旧内存
ps = newp;
i = rhs.i;
return *this;
}
编写赋值运算符时,一个好的模式是:先将右侧运算对象拷贝到一个局部临时对象中,然后再释放左侧运算对象的资源,最后将数据从临时对象中拷贝到左侧运算对象中。
对于行为类似指针的类,我们要拷贝的是指针本身,而不是它指向的 string
。析构函数不能单方面地释放所关联 string
,只有当最后一个指向 string
的 HasPtr
销毁时,它才可以释放 String
。
令一个类展现类似指针的行为最好的方法是用 shared_ptr
来管理类中的资源。但是,有时我们希望直接管理资源。此时,我们可以使用引用计数。
引用计数的工作方式如下:
唯一的难题是确定在哪里存放引用计数。计数器不能直接作为 HasPtr
对象的成员:
HasPtr p1("hello");
HasPtr p2(p1);
HasPtr p3(p1); // 如果计数器是HasPtr的直接成员,p2的计数器将不能正确更新
解决此问题的一种方法是将计数器保存在动态内存中。
class HasPtr {
public:
HasPtr(const string &s = string()):
ps(new string(s)), i(0), use(new size_t(1)){}
HasPtr(const HasPtr& h):
ps(new string(*(h.ps))), i(h.i), use(h.use){ ++*use; }
HasPtr &operator=(const HasPtr &rhs);
~HasPtr();
private:
string *ps;
int i;
size_t *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() {
if (--*use == 0) {
delete ps;
delete use;
}
}
除了定义拷贝控制成员,管理资源的类通常还定义一个名为 swap
的函数。重排元素顺序的算法在交换元素时会调用 swap
,如果一个类定义了自己的 swap
,那么算法将使用自定义版本,否则将使用标准库定义的 swap
。
swap
函数class HasPtr {
public:
friend void swap(HasPtr &, HasPtr &);
};
inline void swap(HasPtr &a, HasPtr &b) {
using std::swap;
swap(a.ps, b.ps);
swap(a.i, b.i);
}
swap
函数应该调用swap
,而不是std::swap
假设我们有一个 Foo
类,它有一个 HasPtr
成员:
void swap(Foo &lhs, Foo &rhs){
// 使用标准库版本swap,而非HasPtr版本
std::swap(lhs.h, rhs.h);
...
}
void swap(Foo &lhs, Foo &rhs){
using std::swap;
// 优先使用HasPtr版本swap
swap(lhs.h, rhs.h);
...
}
至于为什么优先使用
HasPtr
版本swap
,以及为什么using std::swap
没有隐藏HasPtr
版本swap
,后面会解答
swap
定义了 swap
的类通常用 swap
来定义它们的赋值运算符,称作拷贝并交换技术(copy and swap):
// rhs是值传递的
HasPtr &HasPtr::operator=(HasPtr rhs) {
swap(*this, rhs);
return *this;
}
这种技术自动处理了自赋值情况,并且天然就是异常安全的。
class Message;
class Folder;
class Folder {
public:
explicit Folder(string name=string()):
name(name){}
Folder(const Folder &f);
Folder &operator=(const Folder &f);
~Folder();
void addMsg(Message &);
void rmvMsg(Message &);
private:
string name;
set<Message *> messages;
void add_to_messages(const Folder &f);
void remove_from_messages();
};
class Message {
// 每个Message可以出现在多个Folder中
friend class Folder;
public:
// 默认构造函数,folders被隐式初始化为空
explicit Message(const string &str = string()) :
contents(str) {}
// 拷贝构造函数,两个message完全相同,但相互独立
Message(const Message &);
Message &operator=(const Message &);
~Message();
// 将当前message保存到参数的folder中
void save(Folder &);
// 将当前message从参数folder中移除
void remove(Folder &);
private:
// 邮件的内容
string contents;
// 当前message所在的folder集合
set<Folder *> folders;
// 将当前message加入到参数message所在的所有folder中
void add_to_folders(const Message &);
// 从当前message所在的所有文件中删除当前message
void remove_from_folders();
};
void Folder::addMsg(Message &m) {
messages.insert(&m);
}
void Folder::rmvMsg(Message &m) {
messages.erase(&m);
}
void Folder::add_to_messages(const Folder &f) {
for (auto m : f.messages) {
m->save(*this);
}
}
void Folder::remove_from_messages() {
for (auto m : messages) {
m->remove(*this);
}
}
Folder::Folder(const Folder &f) :
name(f.name) {
add_to_messages(f);
}
Folder::~Folder() {
remove_from_messages();
}
Folder &Folder::operator=(const Folder &f) {
remove_from_messages();
name = f.name;
add_to_messages(f);
return *this;
}
void Message::save(Folder &f) {
f.addMsg(*this);
folders.insert(&f);
}
void Message::remove(Folder &f) {
f.rmvMsg(*this);
folders.erase(&f);
}
void Message::add_to_folders(const Message &m) {
for (auto f : m.folders) {
f->addMsg(*this);
}
}
void Message::remove_from_folders() {
for (auto f : folders) {
f->rmvMsg(*this);
}
}
Message::Message(const Message &m) :
contents(m.contents), folders(m.folders) {
add_to_folders(m);
}
Message::~Message() {
remove_from_folders();
}
Message &Message::operator=(const Message &m) {
remove_from_folders();
contents = m.contents;
folders = m.folders;
add_to_folders(m);
return *this;
}