定义一个类时,我们必须对它进行拷贝控制,即控制该类在进行拷贝、赋值、移动和销毁时要进行哪些操作
一个类通过五个特殊的成员函数进行拷贝控制
- 拷贝构造函数
- 拷贝赋值函数
- 移动构造函数
- 移动赋值函数
- 析构函数
拷贝构造和移动构造函数:用同类型初始化对象时该做什么
拷贝和赋值运算符:将一个对象赋予同类型对象时该做什么
析构函数:对象销毁时该做什么
目录在定义任何C++类时,拷贝控制操作都是必要部分。对初学C+的程序员来说,必须定义对象拷贝、移动、赋值或销毁时做什么,这常常令他们感到困惑。这种困扰很复杂,因为如果我们不显式定义这些操作,编译器也会为我们定义,但编译器定义的版本的行为可能并非我们所想。
13.1.1 拷贝构造函数
第一个参数是自身的引用,且任何其他参数都有默认值
class Foo{
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
};
合成拷贝构造函数
如果我们没有定义拷贝函数,编译器会自动生成
和合成默认构造函数不同,即使我们定义了其他构造函数,编译器仍会生成拷贝构造函数
成员类型决定了如何拷贝:
- 内置类型:直接拷贝
- 类类型:调用该类类型的拷贝构造函数
例子:
注意:合成的拷贝构造函数都是接受const引用
拷贝初始化
- 直接初始化:普通的函数匹配
- 拷贝初始化:将等号右侧的对象通过拷贝构造函数拷贝正在创建的对象中去,如果需要会进行类型转换
注:拷贝初始化有时会调用移动构造函数
拷贝初始化合适发生?
- 在使用=定义变量时发生
- 将一个对象作为实参传递给一个非引用类型的形参从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员(参见7.5.5节,第266页)
某些类类型还会对它们所分配的对象使用拷贝初始化。例如,当我们初始化标准库容器或是调用其insert或push成员(参见9.3.1节,第306页)时,容器会对其元素进行拷贝初始化。与之相对,用emplace成员创建的元素都进行直接初始化(参见9.3.1节,第308页)。
参数和返回值
在函数调用过程中,具有非引用类型的参数要进行拷贝初始化。类似的,当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果。
拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。
如果其参数不是引用类型,则调用永远也不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。
拷贝初始化的限制
如果拷贝构造函数是explicit的,无法发生隐式类型转换,那么拷贝初始化和直接初始化就没有什么区别了
编译器可以绕过拷贝构造函数
在进行拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。即编译器允许将下面的代码
string null_book = "999"; //拷贝初始化
改写为:
string null_book("999"); //编译器略过了拷贝构造函数
但是,即使编译器略过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的(例如,不能是private的)。
13.1.2 拷贝赋值运算符
与类控制器其兑现对象的初始化一样,类也可以控制其赋值
Sale_data trans, accum;
trans = accum; //使用Sale_data的拷贝运算符
如果类没有定义拷贝运算符,编译器会为它合成一个
重载赋值运算符
运算符的本质是函数,重载运算符本质是函数的重载
运算符是一个成员函数,对于一个二元运算符(如赋值运算符)
- 参数:左侧对象绑定到隐式的this,右侧对象显示传递
- 返回:对象的引用
//拷贝运算符接受一个与所在类同类型的对象
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
};
赋值运算符一般返回左侧对象的引用
合成拷贝赋值运算符
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符(synthesized copy-assignment operator)。
行为:
将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员,这一工作是通过成员类型的拷贝赋值运算符来完成的。对于数组类型的成员,逐个赋值数组元素。
合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
13.1.3 析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员
析构函数释放对象使用的资源,并销毁对象的非static数据成员。
形式:波浪线+类名,不接受参数
~Foo();//Foo类的析构函数
由于不接受参数,所以无法重载,每个类只能有一个析构函数
析构函数完成什么工作
构造函数先初始化,后执行函数体
析构函数先执行函数体,再销毁对象释放资源
析构过程的注意点
-
销毁成员是按初始化顺序的逆序销毁
-
析构部分是隐式的,无需程序员编写
-
成员具体如何销毁取决于**成员类型*8
-
内置类型没有析构函数,什么也不做
-
智能指针有析构函数,会被销毁;
普通指针没有,所以new动态分配内存时要手动delete
-
类类型执行自己的析构函数
-
什么时候调用析构函数
- 变量在离开其作用域时被销毁。
- 当一个对象被销毁时,其成员被销毁。
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁。
当一个指针或引用离开作用域时,指针变量和引用变量被销毁,但是它们所指向的对象没有被销毁,指向的对象的析构函数不会执行
合成析构函数
一个类未定义自己的析构函数时,编译器会为它自动合成
在析构函数的函数体执行完毕后,对象被销毁
注意:认识到析构函数体自身并不直接销毁成员是非常重要的。成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的,一般用来打印些提示信息。
13.1.4 三/五法则
三个基本操作可以控制类的拷贝操作:拷贝构造、拷贝赋值和析构
在新标准下,还可以定义移动构造函数和移动运算符
C++语言并不要求我们定义所有这些操作:可以只定义其中一个或两个,而不必定义所有。但是,这些操作通常应该被看作一个整体。通常,只需要其中一个操作,而不需要定义所有操作的情况是很少见的。
下面是两条一般性的规则
- 需要析构函数的类也需要拷贝和赋值
- 需要拷贝的类需要赋值,反之亦然
13.1.5 使用=default
用=default
来显式地使用编译器的合成版本
注意参数都是const引用传递
13.1.6 阻止拷贝
定义删除的函数
删除的函数=delete
:
声明了,但不能使用。告诉编译器我们不需要定义此函数
struct NoCopy{
NoCopy() = default; //使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy& operator=(const NoCopy&) = delete; //阻止赋值
~NoCopy(); //使用合成的析构函数
//其他成员
};
与=default的另一个不同之处是,我们可以对任何函数指定=delete(我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default)。虽然删除函数的主要用途是禁止拷贝控制成员,但当我们希望引导函数匹配过程时,删除函数有时也是有用的。
析构函数不能是删除的成员
对于析构函数删除的类而言,我们无法销毁对象,所以
- 无法定义该类型的对象
- 可以动态分配该对象,但无法delete
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
合成的拷贝控制成员可能是删除的
如果一个类有数据成员不能默认构造、拷贝、复制或销毁则对应的成员函数将被定义为删除的:
- 如果类的某个成员的析构函数是删除的或不可访问的(例如,是 private 的),则类的合成析构函数被定义为删除的。
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的。如果类的某个成员的析构函数是删除的或不可访问的,则类合成的拷贝构造函数也被定义为删除的。
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。
- 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器(参见2.6.1节,第65页),或是类有一个const成员,它没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的。
对于有const成员的类
- 编译器不会合成默认构造函数
- 不能使用拷贝和赋值运算(毕竟,合成的赋值运算会赋值给所有成员,而赋值给const成员是不被允许的)
对于有引用成员的类:合成拷贝赋值运算符被定义为删除的。
虽然我们可以将一个新值赋予一个引用成员,但这样做改变的是引用指向的对象的值,而不是引用本身。如果为这样的类合成拷贝赋值运算符,则赋值后,左侧运算对象仍然指向与赋值前一样的对象,而不会与右侧运算对象指向相同的对象。由于这种行为看起来并不是我们所期望的。
private拷贝控制
在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝:
由于析构函数是public的,用户可以定义PrivateCopy类型的对象。但是,由于拷贝构造函数和拷贝赋值运算符是private的,用户代码将不能拷贝这个类型的对象。但是,友元和成员函数仍旧可以拷贝对象。为了阻止友元和成员函数进行拷贝,我们将这些拷贝控制成员声明为private的,但并不定义它们。