《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 ,只有当最后一个指向 stringHasPtr 销毁时,它才可以释放 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)

《C++ Primer》第13章 拷贝控制(二)_第1张图片
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;
}

你可能感兴趣的:(《C++,Primer》,c++)