现在的位置: 首页 > 综合 > 正文

编写new和delete时需固守常规

2013年01月24日 ⁄ 综合 ⁄ 共 4169字 ⁄ 字号 评论关闭

     让我们从operator new开始。实现一致性operator new必得返回正确的值,内存不足时必得调用new_handler函数,必须有对付零字节内存需求的准备,还需避免不慎掩盖正常形式的new——虽然这比较逼近class的接口要求而非实现要求,正常形式的new描述于条款52.

      operator new的返回值十分单纯。如果有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有能力,就遵循条款49描述的规则,并抛出bad_alloc异常。

      然而其实也不是非常单纯,因为operator new 实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来,只有当指向new-handling函数的指针是null,operator new 才会抛出异常。

      奇怪的是C++规定,即使客户要求0bytes,operator new也得返回一个合法指针。这种看似诡异的行为其实是为了简化语言其他成分。下面是non-member operator new伪码(pseudocode):

void* operator new(std::size_t size) throw(std::bad_alloc)
{
	                             //你的operator new可能接受额外参数
	using namespace std;
	if(size ==0)                 //处理0-byte申请 
	{
		size=1;             //将它视为1-byte申请
	}
	while(true)
	{
		//尝试分配size bytes;
		if(分配成功)
		 return (一个指针,指向分配得来的内存);
		
		//分配失败;找出目前的new-handling函数(见下)
		new_handler globalHandler=set_new_handler(0);
		set_new_handler(globalHandler);
		if(globalHandler) (*globalHandler)();
		else throw std::bad_alloc();
	}
}

这里的伎俩是把0bytes申请量视为1bytes申请量。看起来有点黏搭搭地令人厌恶,但做法简单、合法、可行,而且毕竟客户多久才会发出一个0bytes申请呢?

      你可能带着怀疑的眼光斜喵这份伪码(pseudocode),因为其中将new-handling函数指针设为null而后又立刻恢复原样。那是因为我们很不幸地没有任何办法可以直接取得new-handling函数指针,所以必须调用set_new_handler找出它来。拙劣,但有效------至少对单线程程序而言。若在多线程环境中你或许需要某种机锁(lock)以便安全处置new-handling函数背后的(global)数据结构。

    条款49谈到operator new内含一个无穷循环,而上述伪码明白表明出这个循环:"while(true)"就是那个无穷循环。退出此循环的唯一办法是:内存被成功分配或new-handling函数做了一件描述于条款49的事情:让更多内存可用、安装另一个new-handler,卸载new-handler、抛出bad_alloc异常(或其派生物),或是承认失败而直接return 。现在,对于new-handler为什么必须做出其中某些事你应该很清楚了。如果不那么做,operator new的while循环永远不会结束。

    许多人没有意识到operator new成员函数会被derived classes继承,就会导致某些有趣的复杂度。注意上述operator new伪码中,函数尝试分配size bytes(除非size是0)。那非常合理,因为size是函数接受的实参。然而就像条款50所言,写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何derived classes。也就是说,针对class x而设计的operator new,其行为很典型地只为大小刚好为sizeof(x)的对象而设计。然后一旦被继承下去,有可能base
class的operator new被调用用以分配derived class 对象:

class Base
{	
public:	
	static void* operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived:public Base //假设Derived未声明operator new
{

};
Derived* p=new Derived;  //这里调用的是Base::operator new

如果Base class专属的operator new并非被设计用来对付上述情况(实际上往往如此),处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new,像这样:

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

     “等一下!”我听到你大叫,“你忘了检验size等于0这种病态但是可能出现的情况!”。是的,我没检验,但请你收回你的但是。测试依然存在,只不过他和上述的“size与sieof(Base)的检测”融合在一起了。是的,C++在某种秘境中运行,而其中一个秘境就是它所裁定所有非附属(独立式)对象必须有非零大小(见条款37)。因此sizeof(Base)无论如何不能为零,所以如果size是0,这份申请会转交到::operator new手上,后者有责任以某种合理方式对待这份申请。

译注:这里所谓非附属/独立式(freestanding)对象,指的是不以“某对象之base class成分”存在的对象。此处所言的这个规定,可参考《Inside the C++ Object》。

       如果你打算控制class 专属之“array内存分配行为”,那么你需要实现operator new的兄弟版:operator new[]。这个函数通常被称为“array new”,因为很难想出如何发音“operator new[]”。如果你决定写个operator new[],记住唯一需要做的一件是就是分配一块未加工内存(raw memory),因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array将含多少个元素对象。首先你不知道每个对象多大,毕竟base
class的operator new[]有可能经由继承被调用,将内存分配给“元素为derived class对象”的array使用,而你当然知道,derived class 对象通常比其base classs 对象大。
       因此,你不能在Base::operator new[]内假设array的每个元素对象的大小是sizeof(Base),这也意味着你不能假设array的元素对象个数是(bytes申请数)/sizeof(Base)).此外,传递给operator new[]的size_t 参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的arrays可能包含额外空间用来存放元素个数。

      这就是编写operator new时你需要奉行的规矩。operator delete情况更简单。你需要记住的唯一事情就是C++保证“删除null指针永远安全”,所以你必须兑现这项保证。下面是non-member delete的伪码(pseudocode):

void operator delete(void* rawMemory) throw()
{
	if(rawMemory ==NULL) return;       //如果将被删除的是个null指针那就什么都不做
	现在,归还rawMemory所指的内存
}

      这个函数的member版本也很简单,只需要多加一个动作检查删除数量。万一你的class专属的operator new将大小有误的分配行为转交::operator new执行,你也必须将大小有误的删除行为转交::operator delete执行:

class Base    //一如以往,但此刻重点在operaotr delete
{
public:
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	static void operator delete(void* rawMemory,std::size_t size) throw();
};
void Base::operator delete(void* rawMemory,std::size_t size)
{
	if(rawMemory ==0) return;   //检查null指针
	if(size !=sizeof(Base))     //如果大小错误,令标准版operator delete处理此一申请。
	{
		::operator delete(rawMemory,size);
		return;
	}
	现在归还rawMemory所指向的内存;
	return;
}

 

有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t 数值可能不正确。这是“让你的base classes拥有virtual 析构函数”的一个够好的理由;条款7还提过一个更好的理由。我就不岔开话题了,此刻只要你提高警觉,如果你的base classes 遗漏virtual 析构函数,operator delete可能无法正确运作。

请记住

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”。

operator delete 应该再收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”。

抱歉!评论已关闭.