第一章,基础议题

条款4 非必要不提供 default constructor

描述

凡是可以“合理无中生有”的对象类型应该有默认构造,反之如果存在没有外来信息就无法正常初始化的对象,就应该禁止默认构造;

一、没有默认构造的使用限制_1:

由于没有默认构造函数,将无法产生对应类型对象构成的数组

class EquipmentPiece{
public:
EquipmentPiece(int IDNumber);//定义了构造函数,编译器将不再生成默认构造函数
...
}

EquipmentPiece bestPieces[10];// 错误,无法调用EquipmentPiece的构造函数
EquipmentPiece *bestPieces = new EquipmentPiece[10];// 错误

有3个方法可以解决无默认构造无法产生数组的问题:

1、使用non-heap 数组,于是便能够在定义数组时提供必要的自变量:

int ID1,ID2,ID3,...,ID10;
...
EquipmentPiece bestPieces[] = {
EquipmentPiece(ID1),
EquipmentPiece(ID2),
EquipmentPiece(ID3),
...,
EquipmentPiece(ID10),
};

注意: 但是这种方式无法延申到heap数组,也就是用new分配的数组不能用这个方式
补充说明: heap数组是指是通过new或者malloc申请堆空间分配的数组,non-heap数组是指分配在栈或静态存储区系统自动管理的数组

2、更一般化的做法是使用“指针数组”而非“对象数组”

typedef EquipmentPiece* PEP;

PEP bestPieces[10]; //ALL RIGHT! 分配在栈上的指针数组,数组中的每一个元素都是EquipmentPiece类型的指针
PEP *bestPieces = new PEP[10]; //ALL RIGHT! 分配在堆上的指针数组,数组中的每一个元素都是EquipmentPiece类型的指针

//数组中的各指针可用来指向一个个不同的EquipmentPiece对象
for(int i = 0; i< 10; i++)
bestPieces[i] = new EquipmentPiece(ID Number);

注意:以上方式有两个缺点:
(1)必须手动将数组中所有指向的对象删除释放,否则会出现资源泄漏;
(2)需要的内存总量相对较大,因为除了储存Obejct还需要储存指针

3、要解决问题(2)可以先为这个数组分配raw memory(原始内存),然后使用”placement new”(详见条款8)在这块内存上构造一个个对象

void* rawMemory = operator new [](10*sizeof(EquipmentPiece));//分配一块10*sizeof(EquipmentPiece)大小的原始空间

EquipmentPiece *bestPieces = static_cast<EquipmentPiece*>(rawMemory);//定义bestPieces指向块内存(被视为一个EquipmentPiece数组)

for(int i = 0; i < 10; i++)
new (&bestPieces[i]) EquipmentPiece(ID Number); //使用"placement new" 构造这块内存中的EquipmentPiece Objects

补充说明: “placement new” 是不分配新内存,而是在已经分配好的 raw memory(原始内存)上直接构造对象。它解决的是 “内存已存在,只需要创建对象” 的问题。 写法为:

new (内存地址) 类型(初始化值);

使用placement new的缺点其实你已经知道了(不知道自己知道),就是大部分人不了解new还有这种写法,维护起来有点难度,哈哈(如果你没有听过这种new的用法的话);另外一个难点在于删除对象,释放内存的操作:

//将数组中的各对象以构造的相反顺序进行析构(顺序地址导致必须这么做)
for(int i = 9; i >= 0; --i)
bestPieces[i].~EquipmentPiece();

//释放raw memory
operator delete[](rewMemory);//注意!不能用一般的delete []来删除一个new operator获得的指针,其结果未定义,必须使用operator delete[]

二、没有默认构造的使用限制 2:

没有默认构造函数会对一些模板类的设计带来一些挑战,因为在需要设计为数组时就需要这个目标类的默认构造

template<class T>
class Array{
public:
Array(int size);
...
private:
T *data;
};

template<class T>
Array<T>::Array(int size)
{
data = new T[size]; //数组中没有元素都需要调用T::T();默认构造
}

如果谨慎设计模板类,可以消除对默认构造的需求。例如标准的vector template(会产生行为类似“可扩展数组”的各种classes)就不需要其类型参数需要一个默认构造;但是这样的谨慎设计毕竟比较少,因此就带来了一些挑战。所以,究竟要不要提供一个默认构造呢?由此,作者在这里引申出了一点,就是virtual base calsses如果缺乏默认构造,那么与之合作的设计会很麻烦,因为必须要知道构造参数列表,这是一个信息负担

三、是否每个类的设计都应该有默认构造?

依照这样的逻辑:

class EquipmentPiece{
public:
EquipmentPiece(int IDNumber = UNSPECIFIED);//默认构造
...

private:
static const int UNSPECIFIED;//表示没有被指定的ID值
const int mID;
}

这样设计实际会让member functions变得复杂! 博主本人深有体会啊! 因为在需要使用到mID的函数中,你都必须要先判断其是否为UNSPECIFIED无意义值,一旦疏忽,BUG的产生将不可避免;但如果严格设计这个类,符合其存在的逻辑,必须要有初始值才能构造出这个对象,那么将减少很多冗余的判断代码,以及BUG的出现!

因此,仅仅为了编写代码时的便利,就给每一个类设计默认构造会严重减低软件的整体质量!同时也是警醒类的设计者,必须以严谨的态度设计类,确保其构造是符合其行为逻辑的!而不是简单的为了编写代码带来便利而去设计。

另外,上述的错误设计还会为测试代码付出空间代价。

最后一句: 虽然禁止默认构造会带来一些使用的限制,但是当你真的使用了这样的classes,你可以预期,它们产生的对象会被完全地初始化(包括其member functions的使用变得安全)事实上也富有效率。