《C++ Primer》第13章 拷贝控制(二)
参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
13.2 拷贝控制和资源管理(P452)
通常,管理类外资源的类必须定义拷贝控制成员。
为了定义这些成员,我们必须确定此类型对象的拷贝语义。一般来说,有两种选择:使类像一个值或像一个指针。
类的行为像一个值,意味着当我们拷贝一个值时,副本和对象是完全独立的,改变副本对原对象不会有任何影响,反之亦然;类的行为像一个指针,意味着副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。
为了说明这两种方式,我们将定义 HasPtr
类,该类拥有两个成员:一个 int
和一个 string
指针。
13.2.1 行为像值的类(P453)
为了提供类值的行为,对于类管理的资源,每个对象都应该有自己的一份拷贝,为此,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;
}
编写赋值运算符时,一个好的模式是:先将右侧运算对象拷贝到一个局部临时对象中,然后再释放左侧运算对象的资源,最后将数据从临时对象中拷贝到左侧运算对象中。
13.2.2 定义行为像指针的类(P455)
对于行为类似指针的类,我们要拷贝的是指针本身,而不是它指向的 string
。析构函数不能单方面地释放所关联 string
,只有当最后一个指向 string
的 HasPtr
销毁时,它才可以释放 String
。
令一个类展现类似指针的行为最好的方法是用 shared_ptr
来管理类中的资源。但是,有时我们希望直接管理资源。此时,我们可以使用引用计数。
引用计数
引用计数的工作方式如下:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态,计数器的初始值为 1 ;
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员和计数器。拷贝构造函数递增共享的计数器;
- 析构函数递减计数器,如果计数器变为 0 ,则析构函数释放状态;
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器,如果左侧运算对象的计数器变为 0 ,则释放状态。
唯一的难题是确定在哪里存放引用计数。计数器不能直接作为 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;
}
}
13.3 交换操作(P457)
除了定义拷贝控制成员,管理资源的类通常还定义一个名为 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;
}
这种技术自动处理了自赋值情况,并且天然就是异常安全的。
13.4 拷贝控制示例(P460)
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;
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!