2005年5月份,Scott Mayers发布了《Effective C++》第三版。作者根据当前C++的特点和设计模式,对第二版中半数以上的内容作了更新。此等佳作,不敢独享,以肆同好。
什么时候一个空的class不是空的?C++会在何时做些什么事情?如果你不声明它们,编译器会为你声明它们自己的拷贝构造函数、一个赋值运算符和一个析构函数。另外,如果你不声明一个构造函数,编译器还会为你创建一个。所有这些自动生成的函数都是public和inline的。例如,如果你写下:
class Empty {};
这和你写下如下的代码本质上是一样的:
class Empty {
Empty() {…} // default constructor
Empty(const Empty& rhs) {…} // Copy constructor
~Empty() {…} // destructor - whether it's virtual?
// copy assignment operator
Empty& operator=(const Empty& rhs) {}
}
当然,这些函数只有它们真正被需要的时候才会被创建。下面这些情况会使得这些函数被创建:
Empty e1; // default constructor & destructor
Empty e2(e1); // copy constructor
e2 = e1; // copy assignment operator
既然编译器会为你创建这些函数,那么这些函数都做些什么工作呢?默认的构造和析构函数主要是让编译器放置一些执行“幕后工作”的代码,例如调用基类和非静态数据成员的构造和析构函数等。需要注意的是编译器为你生成的这个析构函数并不是虚拟的,除非这个类的基类明确声明了一个虚拟的析构函数。
对于拷贝构造函数和赋值运算符,编译器生成的版本只是简单的copy每一个非静态数据成员。例如,考虑一个名为NamedObject的模板,它可以让你把名字和类型T关联起来。
template <typename T>
class NamedObject {
public :
NamedObject(constchar* 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中对应的成员。由于nameValue的类型是string,并且标准的string有一个拷贝构造函数,所以no2.nameValue就可以通过string的拷贝构造函数完成。另外objectValue是一个整数,对于这个内置类型,简单的bit-copy就可以完成复制的任务了。
其实,如果需要的话,编译器会按照和上面提到的相同的手法来为NamedObject来生成一个赋值运算符。但是,只有当生成的代码在语法和语义都都正确的时候,编译器才会为你执行生成工作,如果其中任何一方面除了问题,编译器就会拒绝为你重载operator =。
例如:如果我们这样定义NamedObject:
template <typename T>
class NamedObject {
public :
NamedObject(const std::string& name, const T& value);
private :
std::string& nameValue;
const T objectValue;
};
之后,下面的代码会怎样呢?
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s; // What should happen?
在复制前,p.nameValue和s.nameValue分别指向不同的string对象。这个复制应该对p.nameValue做怎样的改变呢?直觉上,p.nameValue将会指向s.nameValue所指的string对象。但是这破坏了C++的一条基本的准则,C++不允许引用指向不同的对象。换句话说,难道改变p.nameValue所引用的对象应该要影响到其它对象所引用的字符串吗?这是编译器生成的赋值运算符应该做的事情吗?
C++ 对于这个问题的解答方法是拒绝编译这样的代码。如果你想让含有引用数据成员的类支持赋值功能,那么你就必须自己定义赋值运算符。对于含有const数据成员的类来说,故事是类似的。修改对象中的const成员总是非法的,所以编译器对于如何处理这种问题一无所知。最后,如果基类把operator=声明为private,那么编译器同样会拒绝为派生类生成operator=。毕竟,一方面,即便编译器可以生成,operator=也只能处理派生类中属于基类的那一部分;另一方面,派生类也根本无权访问基类中的private成员。
时时刻刻让自己记住
l 编译器会在必要的时候隐式生成类的默认构造函数、拷贝构造函数、operator=和析构函数。