复制控制
定义一个新类型的时候,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么,这是通过定义特殊成员:复制构造函数、赋值操作符和 析构函数来达到的。如果没有显式定义复制构造函数或赋值操作符,编译器(通常)会为我们定义。
复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
编译器合成的复制控制函数是非常精练的,它们只做必需的工作。但对某些类而言,依赖于默认定义会导致灾难。实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
复制构造函数
复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。与默认构造函数一样 ,复制构造函数可由编译器隐式调用。
使用复制构造函数
- 根据另一个同类型的对象显式或隐式初始化一个对象。
- 复制一个对象将它作为实参传给一个函数。
- 从函数返回时复制一个对象。
- 初始化顺序容器中的元素。[Code3]
- 根据元素初始化式列表初始化数组元素。[Code4]
对象的定义形式
- C++支持两种初始化形式:
直接初始化和复制初始化。复制初始化使用“=”符号,而直接初始化将初始化式放在圆括号中。 - 当用于类类型对象时,初始化的复制形式和直接形式有所不同。[Code1]
- 直接初始化直接调用与实参匹配的构造函数
- 复制初始化首先使用指定
构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。 支持初始化的复制形式主要是为了与C的用法兼容。当情况许可时,可以允许编译器跳过复制构造函数直接创建对象,但编译器没有义务这样做。- 通常直接初始化和复制初始化仅在低级别优化上存在差异。
- 对于不支持复制的类型,或者使用explicit构造函数,不能进行复制初始化。[Code2]
合成的复制构造函数
- 没有定义复制构造函数,编译器就会为我们合成一个。与合成的默认构造函数不同,即使我们
定义了其他构造函数,也会合成复制构造函数。 - 合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。
- 编译器将现在对象的每个
非static成员,依次复制到正创建的对象。只有一个例外,每个成员的类型决定了复制该成员的含义。 - 合成复制构造函数
直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。 - 数组成员的复制是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则
合成复制构造函数将复制数组的每一个元素。定义自己的复制构造函数
- 复制构造函数就是接受单个类类型引用形参(通常用const修饰)的构造函数
Foo(const Foo&) - 因为用于向函数传递对象和从函数返回对象,该构造函数一般不应设置为explicit
- 合成复制构造函数只完成必要的工作。
只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。 - 有些类必须定义复制构造函数对复制对象时发生的事情加以控制。
- 类有一个数据成员是
指针,或者有成员表示在构造函数中分配的其他资源。 - 类在创建新对象时必须做一些特定工作。
- 如果定义了复制构造函数,也必须定义默认构造函数。
禁止复制
- 为了防止复制,类必须显式声明其复制构造函数为private。如果不定义复制构造函数,编译器将合成一个。
- 然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以
声明一个(private)复制构造函数但不对其定义。- 声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。通过声明(但不定义)private复制构造函数,可以禁止任何复制类类型对象的尝试:用户代码中复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。
赋值操作府
与构造函数一样,赋值操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,编译器将为我们合成一个。
Sales_item trans, accum;trans = accum;合成的赋值操作符
- 合成赋值操作符与合成复制构造函数的操作类似。它会执行逐个成员赋值: 右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。[Code5]
- 一般而言,如果类需要复制构造函数,它也会需要赋值操作符。
析构函数
是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。
不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。-
析构函数的调用
- 撤销类对象时会自动调用析构函数
- 动态分配的对象只有在指向该对象的指针被删除时才撤销
- 当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
- 撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数,容器中的元素总是按
逆序撤销。 -
什么时候需要编写显式的析构函数
- 释放资源或者设计者希望在该类对象的使用完毕之后执行的操作
- 如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。
- 合成析构函数
- 与复制构造函数或赋值操作符不同,编译器总是会为我们合成一个析构函数。合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员
在类中声明次序的逆序撤销成员。 - 合成析构函数并不删除指针成员所指向的对象。
- 编写析构函数
- 析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。因为不能指定任何形参,所以
不能重载析构函数。 - 虽然可以为一个类定义多个构造函数,但
只能提供一个析构函数,应用于类的所有对象 - 析构函数与复制构造函数或赋值操作符之间的一个重要区别是,
即使我们编写了自己的析构函数,合成析构函数仍然运行。[Code6]
Code
Code1:类类型的直接初始化和复制初始化
string null_book = "9-999-99999-9"; // copy-initialization string dots(10, '.'); // direct-initialization string empty_copy = string(); // copy-initialization string empty_direct; // direct-initialization
Code2:不支持复制初始化到例子
ifstream file1("filename"); // ok: direct initialization ifstream file2 = "filename"; // error: copy constructor is private // This initialization is okay only if the Sales_item(const string&) constructor is not explicit Sales_item item = string("9-999-99999-9");
Cod
- 析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。因为不能指定任何形参,所以
- 与复制构造函数或赋值操作符不同,编译器总是会为我们合成一个析构函数。合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员
-
- 声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。通过声明(但不定义)private复制构造函数,可以禁止任何复制类类型对象的尝试:用户代码中复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。
- 类有一个数据成员是
- 复制构造函数就是接受单个类类型引用形参(通常用const修饰)的构造函数
- 编译器将现在对象的每个
- 没有定义复制构造函数,编译器就会为我们合成一个。与合成的默认构造函数不同,即使我们
- C++支持两种初始化形式: