当定义一个新类型的时候,需要显式或隐式地指定复制,赋值和撤销该类型的对象时会发生什么----这是通过定义特殊成员:复制构造函数,赋值操作符和析构函数来达到的.如果没有显式定义复制构造函数或赋值操作符,编译器通常会为我们定义.
复制构造函数:是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类型的引用.当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数.当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造函数.
析构函数: 是构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数.析构函数可用于释放对象时构造或在对象的生命周期中获取的资源.不管是否定义了自己的构造函数,编译器都自动执行类中非static数据成员的析构函数.
赋值操作符可以通过制定不同类型的右操作数而重载.右操作数作为类类型时,如果没有编写相应类型的重载函数,编译器将为我们合成一个.
复制构造函数,赋值操作符和析构函数总称为复制控制.编译器自动实现这些操作,但类也可以定义自己的版本.
实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本.有一种特别常见的情况需要类定义自己的复制控制成员:类具有指针成员.
1. 复制构造函数
复制构造函数可用于:
△ 根据另一个同类型的对象显式或隐式初始化一个对象.
△ 复制一个对象,将它作为实参传给函数
△ 从函数返回时复制一个对象
△ 初始化顺序容器中的元素
△ 根据元素初始化式列表初始化数组元素
C++支持两种初始化形式:直接初始化和复制初始化.复制初始化使用=符号,而直接初始化将初始化式放在圆括号中.当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数.复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象.
通过直接初始化和复制初始化仅在低级别优化上存在差异.然而,对于不支持复制的类型,或者使用非explicit构造函数的时候,他们有本质的区别.
复制构造函数
合成复制构造函数:
如果我们没有定义复制构造函数,编译器将会为我们合成一个.合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本.
所谓逐个成员,指的是编译器将现有对象的每个非static成员,依次复制到正创建的对象.每个成员的类型决定了复制该成员的含义.合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制.数组成员的复制是个例外,虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组.复制数组时合成复制构造函数将复制数组的每一个元素.
定义自己的复制构造函数:
复制构造函数就是接受单个类类型引用形参的构造函数.
复制构造函数的定义与其他构造函数一样:它与类同名,没有返回值,可以(而且应该)使用构造函数初始化列表初始化新创建对象的成员,可以再函数体中做任何其他必要的工作.
通常在以下情况下需要对复制对象时发生的事情加以控制:
类有一个数据成员指针,或者有成员表示在构造函数中分配其他资源;
类在创建新对象时必须做一些特定工作.
禁止复制
有些类需要完全禁止复制.
为了防止复制,类必须显式声明其复制构造函数为private.
然而,类的友元和成员仍可进行复制.如果想要连友元和成员中的复制也禁止,就可以声明一个private复制构造函数但不对其定义.
大多数类应定义复制构造函数和默认构造函数.不定义复制构造函数和/或默认构造函数,会严重局限类的使用.不允许复制的类对象只能作为引用传递给函数或从函数返回,它们不能用作容器的元素.
一般来说,最好显式或隐式定义默认构造函数和复制构造函数.只有不存在其他构造函数时才合成默认构造函数.如果定义了复制构造函数,也必须定义默认构造函数.
赋值操作符
重载赋值:
与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个.
赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数.
大多数操作符可以定义为成员函数或非成员函数.当操作符为成员函数时,它的第一个操作数隐式绑定到this指针.
赋值操作符接受单个形参,且该形参是同一类类型的对象.右操作数一般作为const引用传递.
赋值操作符返回对同一类类型的引用.
例如:
class Sales_item
{
public:
Sales_item& operator= (constSales_item &);
private:
std::string isbn;
int units_sold;
double revenue;
};
合成赋值操作符:
与合成复制构造函数的操作类似,它会执行逐个成员赋值,右操作数对象的每个成员赋值给左操作数对象的对应成员.除数组之外,每个成员用所属类型的常规方式进行赋值.对于数组,给每个数组元素赋值.
例如:
Sales_item& Sales_item::operator =(const Sales_item &rhs)
{
isbn = rhs.isbn;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return*this;
}
可以使用合成复制构造函数的类通常也可以使用合成赋值操作符.
如果类需要复制构造函数,它也会需要复制操作符.
析构函数
何时调用析构函数:
1. 撤销类对象时会自动调用析构函数
2. 变量超出作用域时会自动撤销.因此会自动运行析构函数
3. 当对象的引用或者指针超出作用域时,不会运行析构函数.只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数.
4. 撤销一个容器时,也会运行容器中的类类型元素的析构函数,容器中的元素总是按逆序撤销.
何时编写显式析构函数:
许多类不需要显式析构函数,尤其是具有构造函数的类不一定需要定义自己的析构函数.
如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验规则,这个规则常称为三法则.
合成析构函数
编译器总是会为我们合成一个析构函数.合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员在类中声明的次序的逆序撤销成员.
对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象.
合成构造函数并不删除指针成员所指向的对象.
如何编写析构函数
析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参.
虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象.
即使编写了自己的析构函数,合成析构函数仍然运行.