条款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产生出的对象被正确初始化,实现上也富有效率。
版权声明:本文为博主原创文章,未经博主允许不得转载。