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