条款3:绝对不要以多态方式处理数组
1、继承的最重要性质之一就是,你可以通过指向基类的指针或者引用来操作派生类对象。但是如果你通过基类指针或者引用来操作派生类所形成的数组,它几乎绝不会按你预期般地运作。
例子:
#include<iostream>
using namespace std;
class Base{
public:
Base(int i=1){ ib = i; }
friend ostream& operator<< (ostream& os,Base& b){
os << b.ib << " ";
return os;
}
private:
int ib;
};
class Derived :public Base{
public:
Derived(int i = 2, int j = 4,char c='0') :Base(i),id(j),cd(c){}
private:
int id;
char cd;
};
void printArray(ostream& os, Base array[], int numberElements){
for (int i = 0; i < numberElements; ++i)
os << array[i];
}
int main(){
Base bArray[5];
printArray(cout, bArray, 5);//1 1 1 1 1,运行良好
cout << endl;
Derived dArray[5];
printArray(cout, dArray, 5);//2 4 -858993616 2 4,运行异常
cout << endl;
system("pause");
return 0;
}
为什么第二个运行异常?因为array[i]其实是一个指针算术表达式的简写,它所代表的是*(array + i)。(array + i)与array之间的距离是i*sizeof(数组中的对象),printArray函数中参数array声明为类型是Base的数组,所以数组中的每个元素必然是Base对象,所以(array + i)与array之间的距离是i*sizeof(Base)。通常继承类对象比其基类对象大,所以编译器为printArray函数所产生的指针算数表达式,对于继承类对象所组成的数组而言就是错误的。
2、同理,尝试通过一个基类指针删除一个由派生类对象组成的数组,那么上述问题会以另一种形式出现。delete[] array;操作会调用父类的析构函数,而不会调用派生类的析构函数。C++语言规范中说,通过基类指针删除一个由派生类对象组成的数组,其结果未定义。所以多态和指针算术不能混用。数组对象几乎总是会设计指针的算术运算,所以数组和多态不要混用。解决该问题的方法是,避免让一个具体类继承自另一个具体类,这样可以带来许多好处。
条款4:非必要不提供default constructor
一、缺乏default constructor可能出现以下问题:
1、无法产生数组,解决方法由以下三个:
a、使用non-heap数组
b、使用指针数组
c、先为数组分配原始内存,然后使用placement new在分配的内存上构造对象
例子:
#include<iostream>
#include<string>
using namespace std;
class EquipmentPiece{
public:
EquipmentPiece(int IDNumber) :id(IDNumber){}
int getID()const{ return id; }
private:
int id;
};
int main(){
//EquipmentPiece equipArray[5];//错误,不存在默认构造函数
//EquipmentPiece* pEquipArray = new EquipmentPiece[5];//错误,理由同上
//解决方法一,缺点是不能延伸至heap数组
int ID1 = 1, ID2 = 2, ID3 = 3, ID4 = 4, ID5 = 5;
EquipmentPiece equipArray[] = { EquipmentPiece(ID1),
EquipmentPiece(ID2),
EquipmentPiece(ID3),
EquipmentPiece(ID4),
EquipmentPiece(ID5)
};
for (int i = 0; i < 5; ++i)
cout << equipArray[i].getID() << " ";
cout << endl;
//解决方法二,缺点是必须删除数组所指的所有对象,
//所需内存总量比较大,需要一些空间来放指针
typedef EquipmentPiece* PEP;
PEP pep[10];//正确
for (int i = 0; i < 10; ++i){
pep[i] = new EquipmentPiece(i);
cout << pep[i]->getID() << " ";
}
cout << endl;
for (int i = 0; i < 10; ++i)
delete pep[i];
PEP* ppep = new PEP[6];
for (int i = 0; i < 6; ++i){
ppep[i] = new EquipmentPiece(i);
cout << ppep[i]->getID() << " ";
}
cout << endl;
delete[] ppep;
//解决方法三,缺点是大部分程序员不熟悉,维护困难
//对象结束生命时必须手动调用析构函数,最后调用operator delete[]的方式释放rawMemory
void* rawMemory = operator new[](8 * sizeof(EquipmentPiece));
//让pMem指向这块内存,使这块内存被视为一个EquipmentPiece数组
EquipmentPiece* pMem = static_cast<EquipmentPiece*>(rawMemory);
//利用palcement new构造对象
for (int i = 0; i < 8; ++i){
new (&pMem[i]) EquipmentPiece(i);
cout << pMem[i].getID() << " ";
}
cout << endl;
for (int i = 9; i >= 0; --i)
pMem[i].~EquipmentPiece();
operator delete[](rawMemory);
system("pause");
return 0;
}
2、不适用于许多template-based container classes。对templates而言,被实例化的目标类型必须要有一个default constructors。
3、virtual base classes如果缺乏default constructors,与之合作是一种刑法。virtual base classes的自变量必须由欲产生的对象的派生层次最深的class提供。一个缺乏default constructor的virtual base class,要求其所有的derived class都必须了解其意义,并且提供virtual base class的constructor自变量。
二、提供default constructor可能出现以下问题:
1、造成class内的其他member funcitons变得复杂。
2、影响classes的效率。如果成员函数必须测试字段是否真被初始化,其调用者就得为此付出更多的时间,并未测试代码付出空间代价。
总结:如果默认构造函数无法保证对象的所有字段被正确初始化,就不要提供默认构造函数。虽然这可能对classes的使用造成一些限制,但可以保证这样的classes产生出的对象被正确初始化,实现上也富有效率。