Effective C++【一】(Scott Meyers著 侯捷译)

Categories: 工具语言, 读书
Comments: No Comments
Published on: 2011 年 11 月 21 日

一、改变就有的C程序习惯
条款1:尽量以const和inline取代#define。
各种不安全因素,大家熟知,不再解释;编译不能调试宏,如果出错,难以发现。

条款2:尽量以取代或者
一个stdio内的输入输出类型不够多,对新类型束手无策;再一个不进行安全检查(个人感觉有时候这个也比较好用);但有时候确实需要效率的时候,还是可以试用stdio.h的,也视情况而定。

条款3:尽量以new 和delete取代malloc和free。之前的一篇文章内有详细解释和例子,不再赘述。并且一定不要混用,将会对不同C\C++版本造成噩梦般的移植问题。

条款4:尽量使用c++风格注释形式。
如://int temp=a; //swap a and b
//a=b;
//b=temp;
这样是没错的。但是如果:
/* int temp=a; /*swap a and b*/
a=b;
b=temp;*/
将会造成明显的错误,使注释符号提前结束。
但有的老态龙钟的编译器无法理解C++的注释如:
#define LIGHT_SPEED 3e8 //m/sec(in a vacuum)
不过根据条款1所说,还是少用#define吧。

二、内存管理
条款5:使用相同形式的new和delete
string *strA=new string[100];
......
delete strA;
是错误的,因为new出来的是一个数组,而delete的却是一个单一对象,只删除了100个对象里的一个。所以要 delete [] strA;即如果调用new时使用了[],那么删除的时候你也需要使用[]。此时出现了一个新的问题,创造一个类的时候存在一个pointer data member,多个constructors时一定要注意使用统一的new,不然destructor中无法正确抉择如果使用delete了。新问题始终会出现,又如使用typedef时:
typedef string AddressLines[4];
string *pal = new AddressLines;
此时如果使用delete pal;则会造成行为未定义,一定要注意使用的是delete [] pal,告诉编译器你删除的是一个数组。所以为了避免此类错误,尽量避免对数组类型做 typedef。

条款6:记得在destructor中以delete对付pointer members。
类中出现pointer members时,一定要注意对该成员的delete,如果没有initialization,那么你将比较容易发现问题所在,但是如果未delete,虽不会有明显外伤,却有难以察觉的memory leak,终将吃光真个地址使程序过早死亡。另外delete删除null指针也是安全的,所以可以删除任何出现的指针。但也不必太过刻板,没有new过的成员记得的话还是不用删的。特别注意 smart pointer,不要使用delete删掉,也不要删掉任何传值过来的pointer。所以能够使用smart pointer时,还是尽量使用smart pointer吧。

条款7:为内存不足的状况预作准备。
虽然new和malloc不常抛出内存不足的异常,但是一定要有这样的心理准备,
习惯的c语言做法是写一个宏:

#define NEW(PTR,TYPE) \
try{(PTR) = new TTPE;} \
catch(std::bad_alloc&){assert(0);}

上述NEW宏有一个专属缺点:它没把new的无数种使用方法纳入考虑。如:new T,new(constructor arguments) 或者new T[size]等等,甚至clients可以定义自己的operator new。解决方法就是调用头文件提供的内存不足处理函数,用法如下:

typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();

new_handler是个typedef的函数指针,没有传回值,而set_new_handler是个函数,需要一个new_handler参数和该类型的传回值。可以如下使用:

void noMoreMemory(){
cerr<<"Unable to stisfy request for memory\n"; abort();// 异常终止一个进程。中止当前的过程,返回一个错误代码。错误代码的缺省值是3 } int main(){ set_new_handler(noMoreMemory); int *pBig = new int [100000000000]; .... }

如果内存不够用,则会调用noMoreMemory函数。C++并不支持class专属的new-handlers,但我们可以自己完成这种行为,只需让需要的类提供自己的set_new_handler和operator new即可。如下示例:

class X{
	public:
		static new_handler set_new_handler(new_handler p);
		static void * operator new(size_t size);
	private:
		static new_handler currentHandler;
};
new_handler X::currentHandler=0;//缺省初值设为null
new_handler X::set_new_handler(new_handler p){
	new_handler oldHandler = currentHandler;
	currentHandler = p;
	return oldHandler;
}
void * X::operator new(size_t size){
	new_handler globalHandler = std::set_new_handler(currentHandler); //安装X的处理例程
	void *memory;
	try{
		memory=::operator new(size);  //试图配置内存
	}catch(std::bad_alloc&){
		std::set_new_handler(globalHandler); //恢复处理例程
		throw;    //传播exception
	}
	std::set_new_handler(globalHandler);   //恢复处理例程
	return memory;
}

可以理解到你先声明一个处理memory leak的函数,然后把指针传给该类的静态函数指针,然后如果发生memory leak 则会通过重载过的new来进行set_new_handler()的调用,完成该类的专属内存泄露处理过程。如下:

void noMoreMemory();
X::set_new_handler(noMoreMemory);
X *px1= new X; //此时如果new失败则调用new_handler

这段防memory leak的代码对任何类实现都一样,所以自然想到代码重用,只需用template完成这个new 支持类后让需要的进行继承就好了。如下代码:

template<class T>
class NewHandlerSupport{
	public:
		static new_handler set_new_handler(new_handler p);
		static void * operator new(size_t size);
	private:
		static new_handler currentHandler;
};
template<class T>
new_handler NewHandlerSupport<T>::currentHandler=0;//缺省初值设为null
template<class T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p){
	new_handler oldHandler = currentHandler;
	currentHandler = p;
	return oldHandler;
}
template<class T>
void * NewHandlerSupport<T>::operator new(size_t size){
	new_handler globalHandler = std::set_new_handler(currentHandler); //安装X的处理例程
	void *memory;
	try{
		memory=::operator new(size);  //试图配置内存
	}catch(std::bad_alloc&){
		std::set_new_handler(globalHandler); //恢复处理例程
		throw;    //传播exception
	}
	std::set_new_handler(globalHandler);   //恢复处理例程
	return memory;
}
//只需要对需要new支持的类进行继承即可
class X:public NewHandlerSupport<X>{};

memory leak这么处理之后的方便显而易见,这也正是封装继承的魅力所在。这里书上写了一段十分幽默的话,如下:

X的client可以继续保持“对所有隐藏于背后的情节”浑然无知的幸福状态,他们的旧有程序代码继续可以运行。这真是太好了,因为你的clients又一件事永远不会让你失望,那就是……呃~~~忘性奇佳!

喜欢这种程序员的幽默,很喜欢!
另外需要注意的一点是,直到1993年之前,c++还要求operator new无法满足内存需求的时候返回0.目前的此行为只抛出一个是std::bad_alloc exception,但许多c++代码在此修订之前完成的。C++标准委员会不打算放弃这些已经建立起来的程序代码,所以他们提供另一种形式的operator new,继续提供“失败便返回0”的传统,这种形式称为“nothrow”形式,因为过程中不做任何throw action,而且他们在使用new的同时,使用了nothrow objects(定义于之中),如下:

class Widget{...};
Widget *pw1 = new Widget;//如果配置失败则抛出std::bad_alloc,
if(pw1 == 0) ... //这个判断一定会失败!
Widget *pw2 = new(nothrow) Widget; //如果配置失败则返回0,
if(pw2 ==0 ) ... //有效的判定。

不论是用正常形式的new还是 nothrow的形式,最简单的办法还是利用set_new_handler。

条款8:撰写operator new和operator delete时应遵行的公约
当决定写一个自己的operator new的时候有一点十分重要:你的函数的行为应该与缺省的operator new保持一致性。实际一点说,这意味着你的函数应该有正确的返回值,内存不足的时候调用一个错误处理函数,并准备应付“no memory”的需求。你还要避免不经意遮掩了“正常”形式的new。
传回值的部分很简单,如果能够提供足够的内存,返回一个指针指向它;如果不够,参考条款7。这中间有一点细节,因为operator new 会一再尝试配置内存,并在每次失败后调用错误处理函数,这是因为它假象错误处理函数或许能够做某些有益的动作(如释放某些内存)。著有当指向错误处理函数的指针是null时,operator new才会throw a exception。
此外,c++ standard要求,即使用户要求的是0 bytes内存,operator new 也应该传回一个合法的指针。这样的话上一条款写的operator new内应该加入这样一个判断:
if(size ==0){
size = 1;
}
我不知道看过上边的朋友会不会看到这里,我还在考虑要不要在之前的代码里就加上这句,先这样吧。写个完整的伪代码:

void * operator new(size_t size){
if(size ==0){
size = 1;
}
while(true){
attempt to allocate size bytes;
if(the allocation was successful)
return (a pointer to the memory);
//配置不成功,找出目前的错误处理函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler) (*globalHandler)();
else throw std::bad_alloc():
}
}

其实也有点像class的规定,最小的class不是0,而是1。细心的话你会发现伪代码中set_new_handler(0)先把错误处理函数的指针置为null,如果你通过上一条款理解这个set_new_handler函数之后,你就会理解,我们这里是为了获取错误处理函数的指针,他会返回之前的一个错误函数指针。再重新传递给set_new_handler就好了,虽然很绕,但是很有效。另外伪代码中也出现了了while(true),上边有说到程序会一直尝试分配内存直到成功或new_handler做了一些事情:让更多内存可用,或安装一个不同的new_handler,或写出new_handler,或thorw a std::bad_alloc型别的exception,或者直接结束掉程序。
另外operator new 被继承中会引发一些并发症,如果一个针对class X的operator new,次函数的行为几乎总是会被小心的针对“大小为sizeof(X)的对象”校准,不大也不小,然而由于继承之故,有可能subclass大小发生了变化,这时候最好的办法就是将“不正确数量”之内存索求动作丢给标准的operator new去处理,like this:

void * operator new(size_t size){
if(size!=sizeof(Base)) //如果大小错误
return ::operator new(size); //令标准的operator new 处理
......
}

这里不用检查病态的size =0的情况,因为之前提到过,c++中规定所有的class必需非0,如果是0就会默认为1。同样的,new不同大小内存时可以丢给标准operator new去做,delete也可以这样施行。只需谨记一点:c++保证删除一个null pointer永远是安全的。另外需要注意的就是 operator new[]和operator delete[]的情况。

条款9:避免掩盖了new的正规形式
内部(inner) scope 内的一个声明名称,会掩盖外部(outer) scope的相同名称,如果global scope和class scope内部都有函数f,那么member function f会掩盖掉global function f。如下:

void f(); //global function
class X{
public: void f(); //member function
};
X x;
f(); //调用的是global f
x.f(); // 调用的是member f

这是大家都十分熟悉的,并不令人惊讶,然而,如果你为这个class加进一个operator new,就会出现不同的情况:

class X{
public:
void f();
static void * operator new(size_t size, new_handler p);
};
void specialErrorHandler();
X *px1= new(specialErrorHandler) X: //调用X::operator new
X *px2= new X; //错误!想调用global版本的new却不行

这里先不讲解,只是先找到一个解决办法,办法之一就是写一个class专属的operator new,并令它支持“正规”调用形式。

class X{
public:
void f();
static void * operator new(size_t size, new_handler p);
static void * operator new(size_t size){
return ::operator new(size);
}
};
void specialErrorHandler();
X *px1= new(specialErrorHandler) X:
X *px2= new X;

另一种做法就是为operator new 的每一个额外参数提供默认数值。如下:
static void *operator new(size_t size,new_handler p=0);

条款10:如果你写了一个operator new,请对应写一个operator delete
我们回到这个问题基本目的上去,为什么会有人想要撰写自己的new和delete呢,多半是为了效率,虽然标准的new和delete够用,但他们的巨大弹性导致无可避免的需要花费多一些的内存空间,需要大量分配小额空间的尤其如此。
当调用标准的operator new和delete时可能会需要更多的空间,这是为了让new和delete存储一些能够彼此交流的信息。new能够分配任意大小的空间,为了delete能够删除掉这个任意空间,所以它必须知道要删除多少空间,所以就需要在new传回的内存前段“加挂”一些额外数据,说明配置得空的空间大小。这就表示每新申请一次空间有会有额外的空间被浪费掉,所以一次性申请足够的空间来减少申请次数是一个很好的方法来节约空间。下边将展现一个实例,虽然很简单,但是是一个强大样例,它展示了如何更多的节省空间。一定要读懂啊:

class AirplaneRep{.....};//表示一个Airplane object
class Airplane{
	public:
		static void* operator new(size_t size);
	private:
		union{AirplaneRep *rep; Airplane *next;};
		static const int BLOCK_SIZE;
		static Airplane *headOfFreeList;
};
const int Airplane::BLOCK_SIZE=512;
Airplane *Airplane::headOfFreeList=0;
void *Airplane::operator new(size_t size){
	if(size!=sizeof(Airplane))//如果大小错误,则丢给标准new
		return ::operator new(size);
	Airplane *p=headOfFreeList;  //申请个p指针指向free memory头部
	if(p)        //如果p是有效的,则把头部移往free memory list下一个元素
		headOfFreeList = p->next;
	else{
	    //free list已空,配置一块足够大(BLOCK_SIZE个)的内存
		Airplane *newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE*sizeof(Airplane)));
		//组成新的free list,跳过第0个元素,因为第一个元素要返回给申请者
		for(int i=1;i<BLOCK_SIZE-1;i++)
			newBlock[i].next = &newBlock[i+1];
		newBlock[BLOCK_SIZE-1].next=0;//以null结束free list
		p=newBlock;  //将p设为list头部,将headOfFreeList设为下个可运作的小空间
		headOfFreeList=&newBlock[1];
	}
	return p;
}

说明:该类构建Airplane对象,内含一个指针指向一个实例。如果我们要构建free list势必需要一个next指针,但是这里有一个情况就是申请一大块空间,分各个小的目标需求空间,每块空间不是rep指向一个实例对象,就是一个未被使用的内存小空间,所以我们可以用union节省掉这个next指针所占的空间(在该例子中该思路一下减少了一半的空间的浪费)。重载的new操作符能有一次性的分配BLOCK_SIZE个Airplane对象所需要的空间,能够减少BLOCK_SIZE-1次申请空间,也减少了这么多次的new信息存储空间。其优势可以和下边程序对比:

class AirplaneRep{.....};//表示一个Airplane object
class Airplane{
public:
AirplaneRep *rep;
}

两个程序前后能够完成同样的功能,但是内存的使用上差异很明显。并且效率上也快上很多,能快过两个数量级!十分强大的一个方法。
(书中这里又开始幽默了,还有之前的几句,好喜欢程序员的这种幽默啊,有兴趣的请读书)这个条款主要讲的是operator delete,那么如果你这个类没有写delete的重载,结果可想而知,默认的delete根本不知道你竟然偷巧的多申请到那么多空间在你要删除的指针上。所以你一定要注意些new的时候多考虑考虑delete吧。delete参考如下:

class Airplane{
        .......
	public:
		static void operator delete(void *deadObject,size_t size);
};
//接获一块内存,如果它的大小正确,就把它加到free list的前端。
void Airplane::operator delete(void *deadObject,size_t size){
	if(deadObject ==0) return;
	if(size!=sizeof(Airplane)){::operator delete(deadObject);return ;}
	Airplane *carcass=static_cast<Airplane*>(deadObject);
	carcass->next = headOfFreeList;
	headOfFreeList = carcass;
}

这个operator delete虽然未真正删除相应的内存,但是组合起来是构成了一个memory pool,比较有效的利用申请到的空间,重复利用。(这里可以了解一下memory pool的一点思想,个人这么感觉)另外需要说明的一点是:如果被删除的对象系派生自一个缺乏virtual destructor的base class,那么c++传递给operator delete的size_t数值可能是不正确的,所以重载过后的destructor一定要加virtual。
作者后边给出了一个简单的 memory pool的示例:

class pool {
public:
  pool(size_t n);                      // 为大小为n的对象创建
                                       // 一个分配器
  void * alloc(size_t n)  ;            // 为一个对象分配足够内存
                                       // 遵循条款8的operator new常规
  void free(  void *p, size_t n);      // 将p所指的内存返回到内存池;
                                       // 遵循条款8的operator delete常规
  ~pool();                            // 释放内存池中全部内存
};
class airplane {
public:
  ...                              // 普通airplane功能
  static void * operator new(size_t size);
  static void operator delete(void *p, size_t size);
private:
  airplanerep *rep;                 // 指向实际描述的指针
  static pool mempool;              // airplanes的内存池
};
inline void * airplane::operator new(size_t size)
{ return mempool.alloc(size); }
inline void airplane::operator delete(void *p,size_t size)
{ mempool.free(p, size); }

这样 airplane类就干净了很多,无关细节全部屏蔽,没有union,没有free list,常量定义等。就让memory pool的设计者头疼内存管理的琐碎吧,你的工作就是搞好自己class的运作。

ps:本来想着像之前一样一本书看完再发的,但是这本书要好好研读反复揣摩一下,所以先发一部分吧。

我猜你可能也喜欢:

No Comments - Leave a comment

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>


Welcome , today is 星期二, 2017 年 10 月 24 日