04-非必要不提供 default constructor
第一章,基础议题
条款4 非必要不提供 default constructor
描述
凡是可以“合理无中生有”的对象类型应该有默认构造,反之如果存在没有外来信息就无法正常初始化的对象,就应该禁止默认构造;
一、没有默认构造的使用限制_1:
由于没有默认构造函数,将无法产生对应类型对象构成的数组
class EquipmentPiece{ |
有3个方法可以解决无默认构造无法产生数组的问题:
1、使用non-heap 数组,于是便能够在定义数组时提供必要的自变量:
int ID1,ID2,ID3,...,ID10; |
注意: 但是这种方式无法延申到heap数组,也就是用new分配的数组不能用这个方式
补充说明: heap数组是指是通过new或者malloc申请堆空间分配的数组,non-heap数组是指分配在栈或静态存储区系统自动管理的数组
2、更一般化的做法是使用“指针数组”而非“对象数组”
typedef EquipmentPiece* PEP; |
注意:以上方式有两个缺点:
(1)必须手动将数组中所有指向的对象删除释放,否则会出现资源泄漏;
(2)需要的内存总量相对较大,因为除了储存Obejct还需要储存指针
3、要解决问题(2)可以先为这个数组分配raw memory(原始内存),然后使用”placement new”(详见条款8)在这块内存上构造一个个对象
void* rawMemory = operator new [](10*sizeof(EquipmentPiece));//分配一块10*sizeof(EquipmentPiece)大小的原始空间 |
补充说明: “placement new” 是不分配新内存,而是在已经分配好的 raw memory(原始内存)上直接构造对象。它解决的是 “内存已存在,只需要创建对象” 的问题。 写法为:
new (内存地址) 类型(初始化值); |
使用placement new的缺点其实你已经知道了(不知道自己知道),就是大部分人不了解new还有这种写法,维护起来有点难度,哈哈(如果你没有听过这种new的用法的话);另外一个难点在于删除对象,释放内存的操作:
//将数组中的各对象以构造的相反顺序进行析构(顺序地址导致必须这么做) |
二、没有默认构造的使用限制 2:
没有默认构造函数会对一些模板类的设计带来一些挑战,因为在需要设计为数组时就需要这个目标类的默认构造
template<class T> |
如果谨慎设计模板类,可以消除对默认构造的需求。例如标准的vector template(会产生行为类似“可扩展数组”的各种classes)就不需要其类型参数需要一个默认构造;但是这样的谨慎设计毕竟比较少,因此就带来了一些挑战。所以,究竟要不要提供一个默认构造呢?由此,作者在这里引申出了一点,就是virtual base calsses如果缺乏默认构造,那么与之合作的设计会很麻烦,因为必须要知道构造参数列表,这是一个信息负担
三、是否每个类的设计都应该有默认构造?
依照这样的逻辑:
class EquipmentPiece{ |
这样设计实际会让member functions变得复杂! 博主本人深有体会啊! 因为在需要使用到mID的函数中,你都必须要先判断其是否为UNSPECIFIED无意义值,一旦疏忽,BUG的产生将不可避免;但如果严格设计这个类,符合其存在的逻辑,必须要有初始值才能构造出这个对象,那么将减少很多冗余的判断代码,以及BUG的出现!
因此,仅仅为了编写代码时的便利,就给每一个类设计默认构造会严重减低软件的整体质量!同时也是警醒类的设计者,必须以严谨的态度设计类,确保其构造是符合其行为逻辑的!而不是简单的为了编写代码带来便利而去设计。
另外,上述的错误设计还会为测试代码付出空间代价。
最后一句: 虽然禁止默认构造会带来一些使用的限制,但是当你真的使用了这样的classes,你可以预期,它们产生的对象会被完全地初始化(包括其member functions的使用变得安全)事实上也富有效率。
