一个类通过定义五种特殊的成员函数来控制这些操作:
拷贝构造函数
拷贝赋值运算符
移动构造函数
移动赋值运算符
析构函数
13.1拷贝,赋值与销毁
13.1.1拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用(必须是引用类型!),并且没有其他参数或是其他参数都有默认值(即只需要传入一个自身类型的实参即可的构造函数)那么称为拷贝构造函数.
如果我们没有定义,编译器会定义一个合成拷贝构造函数.
合成的拷贝构造函数会将参数的成员逐个拷贝当当前正在创建的对象.
拷贝初始化通常用拷贝构造函数来完成,但如果类中有移动拷贝函数,那么拷贝初始化有时会使用移动构造函数.(粗略来说,区别在于拷贝初始化函数的参数为左值引用&,移动拷贝函数的参数为右值引用&&,后面会再提到)
以std::string为例,以下初始化string即是拷贝初始化:
string s1="hello";
string s2=s1;
string s3=string(10,'w');
以下是直接初始化:
string s4("hello world");
string s5(s4);
这里再粗略来说,一般情况下用等号=来初始化的即为拷贝初始化.(不懂对不对,看书上给出的例子貌似是这样)
同时,不只是用=来定义变量时会发生拷贝初始化,还有以下几种情况会发生拷贝初始化:
将一个对象作为实惨传递给一个非引用类型的形参.
从一个返回类型为非引用类型的函数返回一个对象.
用花括号列表初始化一个数组中的元素或一个聚合类中的成员.(1 所有成员都是public的,2 没有定义任何构造函数,3 没有类内初始值, 4 没有基类没有虚函数 的类为聚合类)
拷贝构造函数的参数必须是引用类型,是因为如果不是引用类型,你们调用拷贝构造函数,则必须拷贝它的实参,为了拷贝实参又必须调用拷贝构造函数……约等于没有终止条件的递归.
13.1.2拷贝赋值运算符
还是以std::string为例,简单看看拷贝赋值运算符是怎么个一回事:
string s1="hello";
string s2="world";
s1=s2;
没错,拷贝赋值运算符就是等号=.因此要定义拷贝赋值运算符,即要重载运算符:
class Text{
public:
Text& operator= (const Text&);
};
返回值为本类的引用(通常来说,不排除有奇特的想法),函数名为operator加上要重载的符号(这里是=),然后参数列表里是本类的引用类型,因为不改变实参的值,因此加上const(非必要,但建议).
同样的,如果我们没定义拷贝赋值运算符,那么编译器会生成一个合成拷贝赋值运算法,默认是将右侧的运算对象的每个非static成员赋予左侧运算符的对应成员.例如:
class Student{
public:
int age;
string name;
//等价于合成拷贝赋值运算符
Student& operator= (const Student& s){
age=s.age;
name=s.name;
return *this; //返回此对象的引用
}
};
13.1.3析构函数
析构函数名字由波浪号和类名构成,没有返回值,没有参数:
class Text{
public:
Text(int a,int b){}; //构造函数
~Text(){}; //析构函数
}
和构造函数一样,没有返回值,函数名由类名构成.构造函数是类出生时(姑且这么说)执行,而析构函数是类死亡时(姑且这么说)执行.
没有参数因此不能被重载,一个类只能有一个析构函数,
和前面的一样,如果我们没有定义析构函数,那么编译器会生成合成构造函数.
析构函数体本身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的.在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的.
13.1.4三/五法则
如果一个类需要一个析构函数,那么一般也会需要拷贝构造函数和拷贝赋值运算符.
如果一个类需要一个拷贝构造函数,那么一般也会需要拷贝赋值运算符.
如果一个类定义了任何一个拷贝操作,那么应该定义所有的五个操作.
13.1.5使用=default
使用=default可以显示地要求编译器来生成的版本,合成的函数将隐式声明为内联的.
class Text{
public:
Text() =default; //显式要求生成编译器生成合成版本的构造函数
Text(const Text&) =default; //显式要求生成编译器生成合成版本的拷贝构造函数
Text& operator=(const Text&) =default; //显式要求生成编译器生成合成版本的拷贝赋值运算符
~Text() =default; //显式要求生成编译器生成合成版本的析构函数
};
13.1.6阻止拷贝
把上面的default换成delete,那么则将对应的特殊函数定义为删除的函数,即即使定义了函数也不能使用它们.delete通知编译器我们不希望定义这些成员.
一般用于禁止拷贝.
析构函数不能是删除的成员!
本质上,当不可能拷贝,赋值,销毁类的成员时,类的合成拷贝控制成员就被定义为删除的.
13.2拷贝控制和资源管理
编写赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中.拷贝完成后,销毁左侧运算对象的现有成员就是安全的了.简单来说就是销毁左侧运算对象资源之前拷贝右侧运算对象.
//这里我们假设Text类的成员name是一个字符串指针
Text& operator=(const Text& r){
auto newName = new string(*r.name); //先拷贝右侧运算对象.
delete name; //销毁左侧对象资源.
name=newName; //将右侧对象数据拷贝到本对象.
return *this; //返回本对象.
}
13.3交换操作
除了开头说的五种拷贝控制成员,管理资源的类通常还定义一个名为swap的函数(重载标准库里的swap)
//假设我们Text类有两个成员变量age和name,其中name是字符串指针
void swap(Text& left,Text& right){
using std::swap;
swap(left.name,right.name);
swap(left.age,left.age);
}
13.4拷贝控制示例
以下直接贴出书中的例子了.
根据Message类的设计编写:
save和remove成员
拷贝控制成员
析构函数
拷贝赋值运算符
swap函数
13.5动态内存管理类
本小节省略.不是因为我看不懂哈
13.6对象移动
标准库容器,string(string虽然操作和标准库容器很接近但不属于容器),shared_ptr类既支持移动也支持拷贝.IO类和unqieu_ptr类可以移动但不能拷贝.
13.6.1右值引用
简单来说,一个&获取的是左值引用,两个&&获取的是右值引用.
左值持久,右值短暂.
右值引用指向将要被销毁的对象.因此我们可以从绑定到右值引用的对象"窃取"状态.(书中原话,没有很理解.这一整章的内容我都迷迷糊糊的.)
13.6.2移动构造函数和移动赋值运算符
移动构造函数和拷贝构造函数基本一致,但拷贝构造函数要的参数是左值引用,而移动构造函数要的参数是右值引用(移动赋值运算符同理).并且在参数列表后加上noexcept关键字,它将会通知标准库我们的构造函数不抛出任何异常.
Text(Text&& t) noexcept{
}
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept.
只要当一个类没有定义任何自己版本的拷贝控制成员,且所有数据成员都可以移动构造或是移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符.简而言之,移动右值,拷贝左值,但如果没有移动构造函数,右值也被拷贝.