编写的每个类几乎都有一个或多个构造函数、一个析构函数和一个赋值运算符。这些是编写一个类所必需的一些函数,控制着类的基本操作,如产生对象并初始化,以及从系统中排除旧对象并对其进行恰当的清理工作,还有赋予对象新值。在这些函数中出错带来很大的负面影响,所以正确地写好这些函数是十分重要的。这些函数构成了类的中枢神经。这一章中将为你介绍怎样编写这些程序才会使你的类更加优秀。
在 C++ 处理过之后,什么时候空类不再是个空类呢?对于一个类来说,如果你自己不手动声明,编译器就会为它声明(编译器版本的)一个copy构造函数、一个赋值运算符、和一个析构函数。而且,在没有声明构造函数的情况下,编译器也将为你声明一个默认构造函数。所有这些函数是public 的并且是 inline 的(参见第 30 条)。举例如下:
class Empty{};
它在本质上和下面这个类等价
class Empty
{
public:
Empty(){...} //default构造函数
Empty(constEmpty& emy){...} //copy构造函数
~Empty(){...} //析构函数 ,下文将分析它是否为虚函数
Empty&operator=(const Empty& emy) //赋值运算符
{...}
};
只有这些函数需要被调用时才会生成,但是需要他们是经常的事情。以下的代码可以生成每一个函数:
Empty e1; //默认构造函数,析构函数
Empty e2(e1); //复制构造函数
e2=e1; //赋值运算符
现在我们知道编译器编写了这些函数,那这些函数做了什么呢?default构造器和析构器主要作用是给编译器一个地方来放置“幕后代码”,就像调用基类和非静态数据成员的构造函数和析构函数。请注意,对于由编译器生成的析构函数,它是non-virtual的,(参 见第 7 条)。
除非这个类继承自一个拥有虚析构函数的基类(这个情况下,析构函数的虚拟性来自它的基类)。
对于复制构造器和赋值运算符而言,编译器所生成的版本仅仅将原对象的非静态数据成员拷贝到目标对象。请参见下边的 NamedObject 模板,它允许你将一个个名称和类型为T的对象产生关联:
tmplate<typename T>
class NameObject
{
public:
NameObject(constchar *name,const T& value);
NameObject(conststd::string& name,const T& value);
...
private:
std::stringnameValue;
TobjectValue
};
由于类中声明了一个构造器,编译器不再为它生成一个默认构造器。这一点很重要。这意味着这个类已经经过你的设计,其构造函数要求实参,这时便不需要担心编译器会在你的类中添加一个无参构造器(即default构造函数)覆盖掉你的版本。
NamedObject 没有声明复制构造器和赋值运算符,所以编译器将会自动生成这些函数(如果被调用的话)。请看下面代码中复制构造器的应用:
NamedObject<int> n1("Smallest Prime Number", 2);
NamedObject<int> n2(n1) ; // 调用复制构造器
由编译器自动生成的这一复制构造器必须要分别使用 n1.nameValue 和 n1.objectValue 来初始化 n2.nameVaule 和 n2.objectValue 。 nameValue 是一个 string ,由于标准字符串类型带有一个复制构造器,所以 n2.nameValue 将通过调用 string 的复制构造器(以 n1.nameValue 作为其参数)得到初始化。另外, NamedObject<int>::ObjectValue 是 int 型的(这是因为对于当前的模板实例来说, T 是 int 型的),而 int 是一个内置类型,所以 n2.objectValue 将通过复制 n1.objectValue 来得到初始化。
由编译器自动生成的NamedObject<int> 的赋值运算符与上述的复制构造器在本质上说行为相同,但一般而言,编译器会评估生成代码是否合法,是否有存在的价值,这两者是赋值运算符生成的前提。如果其中任意一条无法满足,编译器将会拒绝为你的类生成一个 operator= 。
请看下边的示例,如果NamedObject 被定义成这样,nameValue 是一个指向字符串的引用,而objectValue 是一个 const T :
template<class T>
class NameObject {
public:
// 以下的构造器中的 name 参数不再是 const 的了,这是因为现在
//nameValue 是一个引用,它指向非const 的 string 。 char* 参数
// 的构造器已经不复存在了,这是因为引用必须要使用一个 string 。
NamedObject(std::string& name, const T& value);
... // 如前所述,假设没有声明任何 operator=
private:
std::string& nameValue; // 现在是一个引用
const T objectValue; // 现在为 const 的
};
现在请你思考接下来会发生什么事情:
std::string new("abc");
std::string old("def");
NamedObject<int> p(old, 2);
NamedObject<int> s(old, 36);
p = s; // 对于 p 中的数据成员将会发生什么变化
在赋值之前, p.nameValue 和 s.nameValue 都引用了一个 string 对象,尽管不是同一个。那么赋值操作又怎么会影响到 p.nameValue 呢?赋值之后, p.nameValue 应该指向 s.nameValue 所指的那个string? 换句话说,引用是否可以被更改?如果可以的话,我们就开创了一个全新的议题,因为C++不允许“让reference改变指向不同对象”。换个角度说,如果 p.nameValue 所指向的 string 对象被修改了,那么就会影响到其它包含指针或引用指向此 string 对象(换句话说,此次赋值中未直接涉及到的对象),是否可以这样做呢?这些是否是编译器自动生成的赋值运算符应该做的呢?
面对这一问题题, C++ 拒绝编译这类代码。如果你希望为包含引用成员的类赋值,就必须手动定义赋值运算符。对于包含 const 成员的类(比如上文中修改后的 objectValue )也一样。修改 const 成员是非法的,所以编译器无法在一个隐式生成的函数中确定如何处理它们。最终,如果基类中将赋值运算符声明为private 的,那么在派生类中编译器将会把这一隐式的赋值运算符排除在外。毕竟,编译器为派生类自动生成的赋值运算符也要处理基类中相应的部分,但如果这么做了,这些赋值运算符不能调用派生类中无权调用的数据成员。
需要记住的:
编译器会隐式地为一个类生成默认构造函数、复制构造函数、赋值运算符和析构函数