缺省构造函数(指没有参数的构造函数)在C++语言中是一种让你无中生有的方法。构造函数能初始化对象,而缺省构造函数则可以不利用任何在建立对象时的外部数据就能初始化对象。有时这样的方法是不错的。例如一些行为特性与数字相仿的对象被初始化为空值或不确定的值也是合理的,还有比如链表、哈希表、图等等数据结构也可以被初始化为空容器。
但不是所有的对象都属于上述类型,对于很多对象来说,不利用外部数据进行完全的初始化是不合理的。比如一个没有输入姓名的地址簿对象,就没有任何意义。在一些公司里,所有的设备都必须标有一个公司ID号码,所以在建立对象以模型化一个设备时,不提供一个合适的ID号码,所建立的对象就根本没有意义。
在一个完美的世界里,无需任何数据即可建立对象的类可以包含缺省构造函数,而需要数据来建立对象的类则不能包含缺省构造函数。唉!可是我们的现实世界不是完美的,所以我们必须考虑更多的因素。非凡是假如一个类没有缺省构造函数,就会存在一些使用上的限制。
请考虑一下有这样一个类,它表示公司的设备,这个类包含一个公司的ID代码,这个ID代码被强制做为构造函数的参数:
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber);
...
};
因为EquipmentPiece类没有一个缺省构造函数,所以在三种情况下使用它,就会碰到问题。第一中情况是建立数组时。一般来说,没有一种办法能在建立对象数组时给构造函数传递参数。所以在通常情况下,不可能建立EquipmentPiece对象数组:
EquipmentPiece bestPieces[10]; // 错误!没有正确调用
// EquipmentPiece 构造函数
EquipmentPiece *bestPieces =
new EquipmentPiece[10]; // 错误!与上面的问题一样
不过还是有三种方法能回避开这个限制。对于使用非堆数组(non-heap arrays)(即不在堆中给数组分配内存。译者注)的一种解决方法是在数组定义时提供必要的参数:
int ID1, ID2, ID3, ..., ID10; // 存储设备ID号的
// 变量
...
EquipmentPiece bestPieces[] = { // 正确, 提供了构造
EquipmentPiece(ID1), // 函数的参数
EquipmentPiece(ID2),
EquipmentPiece(ID3),
...,
EquipmentPiece(ID10)
};
不过很遗憾,这种方法不能用在堆数组(heap arrays)的定义上。一个更通用的解决方法是利用指针数组来代替一个对象数组:
typedef EquipmentPiece* PEP; // PEP 指针指向
//一个EquipmentPiece对象
PEP bestPieces[10]; // 正确, 没有调用构造函数
PEP *bestPieces = new PEP[10]; // 也正确
在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象:
for (int i = 0; i < 10; ++i)
bestPieces[i] = new EquipmentPiece( ID Number );
不过这中方法有两个缺点,第一你必须删除数组里每个指针所指向的对象。假如你忘了,就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针。
假如你为数组分配raw memory,你就可以避免浪费内存。使用placement new方法在内存中构造EquipmentPiece对象:
// 为大小为10的数组 分配足够的内存
// EquipmentPiece 对象; 具体情况请参见条款8
// operator new[] 函数
void *rawMemory =
operator new[](10*sizeof(EquipmentPiece));
// make bestPieces point to it so it can be treated as an
// EquipmentPiece array
EquipmentPiece *bestPieces =
static_cast(rawMemory);
// constrUCt the EquipmentPiece objects in the memory
// 使用"placement new" (参见条款8)
for (int i = 0; i < 10; ++i)
new (&bestPieces[i]) EquipmentPiece( ID Number );
注重你仍然得为每一个EquipmentPiece对象提供构造函数参数。这个技术(也称为数组到指针的思想array-of-pointers)答应你在没有缺省构造函数的情况下建立一个对象数组。它没有绕过对构造函数参数的需求,实际上也做不到。假如能做到的话,就不能保证对象被正确初始化。
使用placement new的缺点除了是大多数程序员对它不熟悉外(能使用它就更难了),还有就是当你不想让它继续存在使用时,必须手动调用数组对象的析构函数,调用操作符delete[]来释放 raw memory:
// 以与构造bestPieces对象相反的顺序
// 解构它。
for (int i = 9; i >= 0; --i)
bestPieces[i].~EquipmentPiece();
// deallocate the raw memory
operator delete[](rawMemory);
假如你忘记了这个要求或没有用这个数组删除方法,那么你程序的运行将是不可猜测的。这是因为直接删除一个不是用new操作符来分配的内存指针,其结果没有被定义的。
delete [] bestPieces; // 没有定义! bestPieces
//不是用new操作符分配的。
有关new、placement new和它们如何与构造函数、析构函数一起使用的更多信息,请见条款8。
对于类里没有定义缺省构造函数所造成的第二个问题是它们无法在许多基于模板(template-based)容器类里使用。因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。这个要求总是来自于模板内部,被建立的模板参数类型数组里。例如一个数组模板类:
template
class Array {
public:
Array(int size);
...
private:
T *data;
};
template
Array::Array(int size)
{
data = new T[size]; // 为每个数组元素
... //依次调用 T::T()
}
在多数情况下,通过仔细设计模板可以杜绝对缺省构造函数的需求。例如标准的vector模板(生成一个类似于可扩展数组的类)对它的类型参数没有必须有缺省构造函数的要求。不幸的是,很多模板类没有以仔细的态度去设计,这样没有缺省构造函数的类就不能与许多模板兼容。当C++程序员深入领会了模板设计以后,这样的问题应该不再那么突出了。这会花多长时间,完全在于个人的造化。
最后讲一下在设计虚基类时所面临的是要提供缺省构造函数还是不提供缺省构造函数的两难决策。不提供缺省构造函数的虚基类很难与其进行工作。因为几乎所有的派生类在实例化时斗必须给虚基类构造函数提供参数。这就要求所有从没有缺省构造函数的虚基类继续下来的派生类(无论有多远)都必须知道并理解提供给虚基类构造函数的参数含义。派生类的作者是不会企盼和喜欢这种规定的。
因为这些强加于没有缺省构造函数的类上的种种限制,一些人认为所有的类都应该有缺省构造函数,即使缺省构造函数没有足够的数据来初始化一个对象。比如这个原则的拥护者会这样修改EquipmentPiece类:
class EquipmentPiece {
public:
EquipmentPiece( int IDNumber = UNSPECIFIED);
...
private:
static const int UNSPECIFIED; // ID值不确定。
};
这答应这样建立EquipmentPiece对象
EquipmentPiece e; //这样合法
这样的修改使得其他成员函数变得复杂,因为不再能确保EquipmentPiece对象进行有意义的初始化。假设它建立一个因没有ID而没有意义的EquipmentPiece对象,那么大多数成员函数必须检测ID是否存在。假如不存在ID,它们将必须知道怎么犯的错误。不过这经常是不明晰的,很多代码实现什么也没有提供,只是抛出一个异常或调用一个函数终止程序。当这种情形发生时,很难说提供缺省构造函数而放弃了一种保证机制这种做法是否能提高软件的总体质量。
提供无意义的缺省构造函数也会影响类的工作效率。假如成员函数必须测试所有的部分是否都被正确地初始化,那么这些函数的调用者就得为此付出更多的时间。而且还得付出更多的代码,因为这使得可执行文件或库变得更大。它们也得在测试失败的地方放置代码来处理错误。假如一个类的构造函数能够确保所有的部分被正确初始化,所有这些弊病都能够避免。缺省构造函数一般不会提供这种保证,所以在它们能使类变得没有意义时,尽量去避免使用它们。使用这种类的确有一些限制,但是当你使用它时,它也给你提供了一种保证,你能相信这个类被正确地建立和高效地实现。