[转]http://www.cppblog.com/tiandejian/archive/2007/04/17/ecpp_05.html
第二章. 构造器、析构器、赋值运算符
你编写的每个类几乎都有一个或多个构造器、一个析构器、和一个赋值运算符。这没有什么好稀奇的。这些是编写一个类所必需的一些函数,这些函数控制着类的基本操作,其中包括使一个对象由概念变为现实并且确保这一对象得到初始化,以及从系统中排除一个对象并对其进行恰当的清理工作,还有为一个对象赋予一个新的值。在这些函数中出错将为你的类带来深远而重大的负面影响,这自然是令人扫兴的,所以写好这些函数是十分重要的。这些函数构成了类的中枢神经。这一章中将为你介绍怎样编写这些程序才会使你的类更加优秀。
第5条: 要清楚 C++ 在后台为你书写和调用了什么函数
什么时候一个空类在实际上并不是空类呢?我们说, 在 C++ 处理它的时候。对于一个类来说,如果你不自己手动声明一个复制构造器、一个赋值运算符、和一个析构器,编译器就会自动为你声明这些函数。而且,如果你根本没有声明构造器的话,编译器也将为你声明一个默认构造器。所有这些函数将 是 public 的并且是 inline 的(参见第 30 条)。举例说,如果你编写了:
它在本质上讲与下边这个类是等价的:
class Empty {
public:
Empty() { ... } // 默认构造器
Empty(const Empty& rhs) { ... } // 拷贝构造器
~Empty() { ... } // 析构器 — see below
// 下文将分析它是否为虚函数
Empty& operator=(const Empty& rhs) // 赋值运算符
{ ... }
};
这些函数只有在需要的时候才会生成,但是需要他们是经常的事情。以下的代码可以生成每一个函数:
Empty e1; // 默认构造器
// 析构器
Empty e2(e1); // 复制构造器
e2 = e1; // 赋值运算符
现在我们知道编译器为你编写了这些函数,那么这些函数是做什么的呢?默认构造器和析构器主要作用是为编译器提供一个放置“幕后代码”的空间,“幕后代码”完成的是诸如对于基类和非静态数据成员的构造器和析构器的调用。请注意,对于由编译器生成的析构器,除非这个类继承自一个拥有虚析构器的基类(这个情况下,析构器的虚拟性来自它的基类),这个析构器并不是虚函数(参 见第 7 条 )。
对于复制构造器和赋值运算符而言,编译器所生成的版本仅仅由原对象复制出所有的非静态数据成员到目标对象。请参见下边的 NamedObject 模板,它的功能是:为类型 T 的对象。
template<typename T>
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};
由于 NamedObject 中声明了一个构造器,编译器则不会为你自动生成一个默认构造器。这一点很重要。这意味着如果这个类已经经过你认真仔细的设计,你认为它的构造器必须包含参数,这时候你便不需要担心编译器会违背你的意愿,轻率地在你的类中添加一个无参构造器。
NamedObject 没有声明复制构造器和赋值运算符,所以编译器将会自动生成这些函数(如果需要)。请看下面代码中队复制构造器的应用:
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1) ; // 调用复制构造器
由编 译器自动生成的这一复制构造器必须要分别使用 no1.nameValue 和 no1.objectValue 来初始化 no2.nameVaule 和 no2.objectValue 。 nameValue 是一个 string ,由于标准字符串类型带有一个复制构造器,所以 no2.nameValue 将通过调用 string 的复制构造器(以 no1.nameValue 作为其参数)得到初始化。另外, NamedObject<int>::ObjectValue 是 int 型的(这是因为对于当前的模板实例来说, T 是 int 型的),而 int 是一个内建类型,所以 no2.objectValue 将通过复制 no1.objectValue 来得到初始化。
由编译器自动生成的 NamedObject<int> 的赋值运算符与上述的复制构造器在本质上说拥有一致的行为,但是大体上讲,就像我前面描述过的,编译器会评估生成代码是否合法,是否有存在的价值,这两者是赋值运算符生成的前提。如果其中任意一条无法满足,编译器将会拒绝为你的类生成一个 operator= 。
请看下边的示例,如果 NamedObject 被定义成这样, nameValue 是一个指向字符串的引用,而 objectValue 是一个 const T :
template<class T>
class NamedObject {
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 newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2); // 在我最初编写这段代码的时候
// 我们的狗 Persephone 正要度
// 过她的两周岁生日
NamedObject<int> s(oldDog, 36); // 我家的狗 Satch (在我小时候
// 养的)如果她现在还活着的话
// 应该有 36 岁了
p = s; // 对于 p 中的数据成员将会发生 // 什么呢?
在赋值之前, p.nameValue 和 s.nameValue 都引用了一个 string 对象,尽管不是同一个。那么赋值操作又怎么会影响到 p.nameValue 呢?在赋值之后, p.nameValue 是否引用了以前由 s.nameValue 所引用的 string 呢?换句话说,引用是够可以被更改呢?如果可以的话,我们就开创了一个全新的议题,因为 C++ 并 没有提供让一个引用去援引其它对象的方法。换个角度说,如果 p.nameValue 所引用的 string 对象被修改了,那么就会影响到其它包含指针或引用指向此 string 对象(换句话说,此次赋值中未直接涉及到的对象),是否可以这样做呢?这些是否是编译器自动生成的赋值运算符应该做的呢?
面对这一难题, C++ 拒绝编译这类代码。如果你希望为包含引用成员的类赋值,就必须亲自手动定义赋值运算符。对于包含 const 成员的类(比如上文中修改后的 objectValue )也一样。修改 const 成员是非法的,所以编译器无法在一个隐式生成的函数中确定如何处理它们。最终,如果基类中将赋值运算符声明为 private 的,那么在派生类中编译器将会把这一隐式的赋值运算符排除在外。毕竟,编译器为派生类自动生成的赋值运算符也要处理基类中相应的部分,但是如果这样做了,这些赋值运算符不能调用派生类中无权调用的数据成员。
需要记住的
l 编译器会隐式生成一个类的默认构造器、复制构造器、赋值运算符和析构器。