拷贝控制

基本概念

定义一个类时的5个基本函数:
拷贝构造函数移动构造函数定义了当用同类型的另一个对象初始化当前对象的时候发生什么。拷贝赋值运算符移动赋值运算符定义了将一个对象赋予同类型的另一个对象时发生什么。析构函数则定义了此类型对象销毁时做什么。这些操作统称为拷贝控制操作。(很多时候我们会依赖于编译器提供的默认定义)
例子:

1
2
3
4
Person p;
Person p1=p; //调用拷贝构造函数
Person p2;
p2=p; //调用拷贝赋值函数

拷贝、赋值与销毁

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且其他任何额外参数都有默认值,则这个构造函数是拷贝构造函数。同时,拷贝构造函数的第一个参数也就是对自身类型的引用几乎都是const类型的。

例如:

1
2
3
4
5
class fool{
public:
fool(); //默认构造函数
fool(const fool&); //拷贝构造函数
};

拷贝初始化

直接初始化与拷贝初始化的几个例子:
1
2
3
4
5
string dots(10,'.');    //直接初始化
string s(dots); //直接初始化
string s2=dots; //拷贝初始化
string null_book="9999999"; //拷贝初始化
string nines=string(100,'9'); //拷贝初始化

使用直接初始化,事实上是要求编译器选择与我们提供的参数最匹配的构造函数。当使用拷贝初始化时,编译器会将右侧运算对象拷贝到正在创建的对象中,必要时还会进行类型转换。

发生拷贝初始化的几种情况:

·将一个对象作为实参传递给一个非引用类型&的形参
·从一个返回类型为非引用类型的函数返回一个对象
·用花括号列表初始化一个数组中的元素或者一个聚合类中的成员
·某些标准库容器的insert或push操作,而用emplace成员创建的元素都会进行直接初始化

拷贝赋值运算符

赋值运算符其实就是一个名为operator=的函数,这个函数也有一个返回类型和参数列表。同时,赋值运算符必须定义为成员函数,其左侧运算对象绑定到了隐式的*this参数,右侧运算对象则作为显示参数传递。
赋值运算符接受一个与其所在类相同类型的参数,并通常返回一个指向其左侧运算对象的引用。
例子:

1
2
3
4
5
class Foo{
public:
Foo& operator=(const Foo&);
//...
};

如果一个类没有定义自己的拷贝赋值运算符,那么编译器会为它生成一个合成拷贝赋值运算符,它会将右侧对象的每个非static值赋予左侧运算对象的相应成员。

析构函数

析构函数执行与构造函数相反的操作:销毁对象的非static成员。
析构函数是类的一个成员函数,名字由波浪号+类名构成,没有返回值也不接受任何参数,也不能被重载
例子:

1
2
3
4
5
class Foo{
public:
~Foo();
//...
};

只要一个对象被销毁,就会自动调用其析构函数。一般来说,大多数对象在离开时都会被自动销毁,但是动态分配的对象并不会被销毁(当指向一个对象的引用或指针离开作用域时,析构函数不会执行),此时就必须依赖自定义的析构函数。
例如:

1
2
3
4
5
6
7
8
9
10
class M
{
private:
int *p; /* data */
public:
M(int n) { p = new int[n]; }
~M() { delete[] p; }//默认析构函数只会销毁p,而不会销毁动态分配的p指向的内存
};

delete P;//对p指向的对象调用析构函数

同样的,如果没有显示定义,编译器也会为类定义一个合成析构函数。
如果一个类需要一个析构函数,那么它也几乎必须拥有一个拷贝构造函数和一个拷贝赋值函数。 这是因为默认的构造与赋值函数只会进行简单的复制操作,而简单的指针复制会使多个对象中的指针指向同一个地址,如果其中一个指针进行了销毁操作,那么其他指针也会随之变为无效指针。

阻止拷贝(定义删除的函数)

对于某些类而言,希望禁止拷贝构造与拷贝赋值操作,比如iostream类,这种情况下,我们可以将拷贝构造函数与拷贝赋值函数定义为删除的函数(deleted function):我们声明了他们,但是不能以任何方式使用他们,在函数参数列表后面加上=delete来指出这一点。
例如:

1
2
3
4
struct Nocopy{
Nocopy()-default;
Nocopy(const Nocopy&)=delete; //阻止拷贝构造函数
Nocopy &operator=(const Nocopy&)=delete; //阻止拷贝赋值

注意:析构函数不能是删除的成员
在某些情况下,合成的构造/赋值函数也可能是删除的。
在新标准发布之前,很多时候回通过将构造/赋值运算符声明为private的来阻止拷贝。