当定义一个类时,我们显示地或隐世地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。以上操作为拷贝控制操作。
class Foo{
friend void swap(Foo&,Foo*);//将swap定义为友元函数,并在外部重载
public:
Foo();//默认构造函数
//或者Foo()=default;显示要求生成合成版本
Foo(const Foo&);//拷贝构造函数
Foo& operator=(const Foo&);//重载的拷贝赋值运算符
~Foo();//析构函数
//或者~Foo()=default;显示要求生成合成版本
private:
string* s;
};
拷贝构造函数
- 在几种情况下都会被隐式使用,因此通常拷贝构造函数不应该是
explicit
的 - 若一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
构造函数
- 成员初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化。
拷贝赋值运算符
- 如果将一个对象赋予它自身,赋值运算符必须能正常工作
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
- 要考虑其异常安全的,即当异常发生时,能将左侧运算对象置于一个有意义的状态
//其主要针对防止内存不够用时,依旧能够保持左侧运算对象的原始状态
Foo& Foo::operator=(const Foo& tmp){
//if (this!=&tmp){
//阻止对自身的赋值
auto newS = new string(*tmp.s);//拷贝底层string
delete s;//释放旧内存
s=newS;//从右侧运算对象拷贝数据到本对象
return *this;//返回本对象
//}
}
析构函数
- 由于析构函数不接受参数,因此它不能被重载。对于一个定类,只会有唯一一个析构函数。
- 首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。(与构造时相反)
- 由于智能指针是类类型,所以具有析构函数,因此,与普通指针不同,智能指针成员在析构阶段会被自动销毁。
- 析构函数会自动运行
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显示地还是隐式地。
阻止拷贝
通过将拷贝构造函数和拷贝赋值运算符定义为删除函数来组织拷贝,即声明了他们,但不能以任何方式使用:
struct NoCopy{
NoCopy()=default;//使用合成的默认构造函数
NoCopy(const NoCopy&)=delete;//阻止拷贝
NoCopy&operator=(const NoCopy&)=delete;//阻止赋值
~NoCopy()=default;//使用合成的析构函数
};
=delete
必须出现在函数第一次申明的时候,而=default
不是。- 析构函数不能是删除的成员,除非是动态分配(可是不能删除)
交换函数swap
- 对于指针,交换指针而非指向的对象
移动构造函数和std::move
- 有些标准库类,包括string,都定义了所谓的”移动构造函数“,但是关于string的移动构造函数如何工作和实现的细节都尚未公开。
- 移动构造函数通常是将资源从给定对象”移动“而不是拷贝到正在创建的对象,且保证原始的string仍然保持一个有效的、可析构的状态。
move
定义在utility头文件中。- 若对象在拷贝后立即就被销毁,则移动而非拷贝对象会大幅度提升性能
- IO类或unique_ptr这样的类,都包含不能被共享的资源(如指针或IO缓冲),因此
通过调用move
函数来获得绑定到左值上的右值引用,其相当于告诉编译器,我们有一个左值,但是我们希望像一个右值一样处理。这意味着,除了对rr1赋值或销毁外,将不再使用它
int &&rr3 = std::move(rr1);//ok
StrVec r = std::move(rO);//rO为原始的StrVec对象,且StrVec定义了移动构造函数
使用std::move
而非move
可以避免潜在的名字冲突。
右值引用
为了支持移动操作而引入,通过&&
来获得右值引用。
性质:只能绑定到一个将要销毁的对象
与左值引用&
相反,能够将右值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式上,但不能绑定到一个左值上:
int i=42;
int &r = i;//ok
int &&rr = i;//no,不能绑定到一个左值上
int &r2 = i*42;//no,i*42是一个右值
const int &r3 = i*42;//ok,可以将一个const引用绑定到一个右值上
int &&rr2 = i*42;//ok
自定义一个移动构造函数
StrVec::StrVec(StrVec&& s) noexcept//移动操作不应抛出任何异常
//成员初始化器接管s中的资源
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//令s进入这样的状态——对其运行析构函数是安全的
s.elements = s.first_free=s.cap=nullptr;
}
- 类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。任何额外的参数必须有默认实参。
- 不同于拷贝构造函数,这个引用参数在移动构造函数中是一个右值引用。且不分配任何新的内存,在接管完内存后,需要将源对象的指针都置为
nullptr
。 - 除了完成资源移动,移动构造函数还需确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。
移动赋值运算符
StrVec & StrVec::operator=(StrVec &&rhs) noexcept{
//直接检测自赋值
if (this != &rhs){
free();//释放已有元素
elements=rhs.elements;//从rhs接管资源
first_free=rhs.first_free;
cap=rhs.cap;
//将rhs置为可析构状态
rhs.elements=rhs.first_free=rhs.cap=nullptr;
}
return *this;
}
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译其才会为它合成移动构造函数或移动赋值运算符,
否则,合成移动构造函数或移动赋值运算符是删除函数,阻止调用。