Part 6: 拷贝控制(第13章)
// @author: gr
// @date: 2015-01-08
// @email: forgerui@gmail.com
一、拷贝、赋值与销毁
-
拷贝构造函数
-
拷贝赋值运算符
-
析构函数
析构函数自身并不直接销毁成员,而是在析构函数之后隐含的析构阶段中被销毁的。 -
使用
=default
修饰成员时,要求编译器生成合成的版本。 -
阻止拷贝
在新标准下,使用删除的函数来阻止拷贝,虽然声明了它,但不能以任何方式使用它。struct NoCopy{ NoCopy() = delete; Nocopy(const NoCopy&) = delete; //阻止拷贝 NoCopy& operator=(const Nocopy&) = delete; //阻止赋值 };
将拷贝函数声明为
private
,阻止调用,并且只声明不实现。
二、拷贝控制和资源管理
-
一旦一个类需要析构函数,那么它几乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。
-
定义拷贝操作,使类的行为看起来像一个值或指针。像值的类拥有自己的状态,像指针的类则共享状态(当拷贝一个这种类的对象时,副本和原对象使用相同的底层数据)。
-
行为像值的类,有两个数据成员
string
、int
class HasPtr{ public: HasPtr(const std::string &s = std::string()) : ps(new string(s)), i(0){} HasPtr(const HasPtr& p) : ps(new string(*p.ps)), i(p.i){} HasPtr& operator= (const HasPtr&); ~HasPtr(){ delete ps;} private: int i; std::string* ps; }; HasPtr& HasPtr::operator=(const HasPtr& rhs){ std::string* newp = new std::string(*rhs.ps); //先创建一个新拷贝之后再delete,否则如果传入自己,delete会使rhs.ps失效,再拷贝就什么也没有了。 delete ps; ps = newp; i = rhs.i; return *this; }
注意:
- 如果一个对象赋予它自身,赋值运算符必须能正确工作。
- 大多数赋值运算符组合了析构函数和构造函数的工作。
-
行为像指针的类,多个对象共享一份资源,需要使用引用计数。可以用
shared_ptr
,也可以自己实现。class HasPtr{ public: HasPtr(const std::string& s = std::string()) : ps(new string(s)), use(new std::size_t(1)), i(0){} HasPtr(const HasPtr& rhs) : ps(rhs.ps), use(rhs.use), i(rhs.i){ ++*use; } HasPtr& operator=(const HasPtr&); ~HasPtr(); private: int i; string* ps; std::size_t* use; }; //析构函数 HasPtr::~HasPtr(){ if (--*use == 0){ delete ps; delete use; } } //拷贝运算符 HasPtr& HasPtr::operator=(const HasPtr& rhs){ ++*rhs.use; //先判断是否是最后一个拥有资源的类,如果是,则删除资源 if (--*use){ delete ps; delete use; } ps = rhs.ps; use = rhs.use; i = rhs.i; return *this; }
三、交换操作SWAP
-
swap
函数应该调用swap
,而不是std::swap
定义类自己的swap
函数,如下:class HasPtr{ public: friend void swap(HasPtr& lhs, HasPtr& rhs); }; inline void swap(HasPtr& lhs, HasPtr& rhs){ using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i, rhs.i); }
每个
swap
函数都应该是未加限制的,这样如果存在类型特定的swap
版本,则优先于特定的swap
版本。 -
在赋值中使用
swap
HasPtr& HasPtr::operator=(HasPtr rhs){ //参数是一个值,而不是引用 swap(*this, rhs); return *this; }
四、对象移动
-
新标准中的一个特性是可以移动而非拷贝对象的能力。这样会大幅度提升性能。
-
为了支持移动操作,新标准引入了一种新的引用类型,右值引用,就是必须绑定到右值的引用。通过
&&
而不是&
来获得引用。
int &&rr = i * 42;
int &&rr1 = 42; //正确:字面常量是右值
int &&rr2 = rr1; //错误:变量是左值 -
标准
move
函数
可以通过move
显式地将右值引用绑定到一个左值上。int &&rr3 = std::move(rr1);
-
移动构造函数和移动赋值运算符
移动构造函数的参数是该类类型的一个右值引用。StrVec::StrVec(StrVec &&s) noexcept //移动操作不应抛出任何异常 : elements(s.elements), first_free(s.first_free), cap(s.cap) { //令s进入这样的状态,对其运行析构函数是安全的 s.elements = s.first_free = s.cap = nullptr; //置为nullptr后,便可析构rhs,不会影响 } //移动赋值运算符 StrVec& StrVec::operator=(StrVec&& rhs) noexcept{ if (this != &rhs){ free(); elements = rhs.elements; first_free = rhs.first_free; cap = rhs.cap; rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }