复制控制:
当定义一个新类型的时候,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么——这是通过定义特殊成员:复制构造函数、赋值操作符和析构函数来达到的。如果没有显式定义复制构造函数或赋值操作符,编译器(通常)会为我们定义。
复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或函数返回该类型的对象时,将隐式使用复制构造函数。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。不管类是否定义了自己的析构函数,编译器都自动执行类中非 static 数据成员的析构函数。
实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
复制构造函数可用于:
根据另一个同类型的对象显式或隐式初始化一个对象。
• 复制一个对象,将它作为实参传给一个函数。
• 从函数返回时复制一个对象。
• 初始化顺序容器中的元素。
• 根据元素初始化式列表初始化数组元素。
复制初始化使用 = 符号,而直接初始化将初始化式放在圆括号中。
如果使用常规的花括号括住的数组初始化列表(第 4.1.1 节)来提供显式元素初始化式,则使用复制初始化来初始化每个元素。
Sales_item primer_eds[] = { string("0-201-16487-6"),
string("0-201-54848-8"),
string("0-201-82470-1"),
Sales_item()
};
禁止复制:
有些类需要完全禁止复制。例如,iostream 类就不允许复制(第 8.1 节)。如果想要禁止复制,似乎可以省略复制构造函数,然而,如果不定义复制构造函数,编译器将合成一个。 为了防止复制,类必须显式声明其复制构造函数为 private。
如果复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也
禁止,就可以声明一个(private)复制构造函数但不对其定义。 声明而不定义成员函数是合法的。
合成赋值操作符与合成复制构造函数的操作类似。它会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。
例如,Sales_item 的合成赋值操作符可能如下所示:
// equivalent to the synthesized assignment operator
Sales_item&
Sales_item::operator=(const Sales_item &rhs)
{
isbn = rhs.isbn; // calls string::operator=
units_sold = rhs.units_sold; // uses built-in int assignment
revenue = rhs.revenue; // uses built-in double
assignment
return *this;
}
动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。当对象的引用或指针超出作用域时,不会运行析构函数。只有
删除指向动态分配对象的指针或实际对象(而不是对象的引用)
超出作用域时,才会运行析构函数。
何时编写显式析构函数:
仅在有些工作需要析构函数完成时,才需要析构函数。析构函数通常用于释放在构造函数或在对象生命期内获取的资源。如果类需要析构函数,则它也需要赋值操作符和复制构造函
数,这是一个有用的经验法则。这个规则常称为三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员。
合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。