学习类如何控制对象拷贝、赋值、移动或销毁时做什么。类通过一些特殊的成员函数控制这些操作,包括:拷贝构造函数、移动构造函数、拷贝赋值运算符、移动运算符以及析构函数。
当定义一个类时,我们显式或隐式地指定在此类型非对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数和析构函数。
拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数定义了当此类型对象销毁时做什么。
称这些操作为拷贝控制。
1、拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数通常不应该是explicit的
class Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&) //拷贝构造函数
}
合成拷贝构造函数
如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。
一般情况下,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器会从给定对象中一次将每个非static成员拷贝到正在创建的对象中。
每个成员的类型决定了它如何拷贝:对类类型的成员,会使用期拷贝构造函数来拷贝;内置类型的成员则直接拷贝。
拷贝初始化通常用拷贝构造函数来完成。
拷贝构造函数使用的情况:
(1) 用=定义变量时
(2) 将一个对象作为实参传递给一个非引用类型的形参
(3) 从一个返回类型为非引用类型的函数返回一个对象
(4) 用花括号列表初始化一个数组中的元素或一个聚合类的成员
(5) 初始化标准库容器或调用其insert/push操作时,容器会对其元素进行拷贝初始化
拷贝构造函数用来初始化非引用类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。
编译器可以绕过拷贝构造函数,直接创建对象,但拷贝/移动构造函数必须是存在且可访问的(例如不能是private的)
2、拷贝赋值运算符
重载赋值运算符
重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。重载运算符的参数表示运算符的运算对象。
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
//
}
赋值运算符通常应该返回一个指向其左侧运算对象的引用
合成拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
3、析构函数
构造函数初始化对象的非static数据成员,可能还做一些其他的工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员
class Foo{
public:
~Foo(); //析构函数
//…
}
由于析构函数不接受参数,因此他不能被重载,对一个给定的类,唯一一个析构函数。
析构函数完成什么工作
析构函数有一个函数体和一个析构部分。在一个析构函数中,首先执行函数体,然后销毁成员,按成员初始化顺序的逆序销毁。通常,析构函数会释放对象在生存期分配的所有资源。
成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
与普通指针不同,智能指针是类类型,具有析构函数,智能指针成员在析构阶段会被自动销毁
什么时候调用析构函数
(1) 变量离开其作用域
(2) 当一个对象被销毁时,其成员被销毁
(3) 容器(标准库容器或数组)被销毁时,其元素被销毁
(4) 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
(5) 对于临时对象,当创建它的完整表达式结束时被销毁
当指向一个对象的引用或指针离开作用域时,析构函数不会执行
三/五法则
需要析构函数的类也需要拷贝和赋值操作:如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
需要拷贝操作的类也需要赋值操作,反之亦然:如果一个类需要一个拷贝赋值函数,几乎可以肯定它也需要一个拷贝赋值运算符;反之亦然——如果一个类需要拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
使用default
可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本。当在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联函数。
阻止拷贝
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论隐式还是显式的。
iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。
删除的函数
可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在参数的参数列表后面加上=delete来指明将其定义为删除的。
析构函数不能使删除的
合成的拷贝控制成员可能是删除的
对某些类来说,编译器将这些合成的成员定义为删除的函数:
(1) 类的某个成员的析构函数是删除的或不可访问的,则类的合成析构函数被定义为删除的;
(2) 如果类的某个成员的拷贝构造函数是删除的或不可访问的(如private的)则类的合成析构函数被定义为删除的,类的某个成员的析构函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的;
(3) 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的
(4) 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器,或是类有一个const成员,它没有类内初始化器且器类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的
这些规则的含义是:如果一个类有数据成员不能默认构造、拷贝、赋值或销毁,则对应的成员函数将被定义为删除的
在C++11标准之前,类通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝。
浙公网安备 33010602011771号