【more effective c++读书笔记】【第1章】基础议题(2)

条款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产生出的对象被正确初始化,实现上也富有效率。


版权声明:本文为博主原创文章,未经博主允许不得转载。

原文地址:https://www.cnblogs.com/ruan875417/p/4785428.html