effective C++笔记--定制new和delete(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rest_in_peace/article/details/84872083

编写new和delete时需固守常规

. 在编写自己的operator new和operator delete时,需要遵守一些规则,先从operator new开始:实现一致性的operator new必需要返回正确的值;内存不足时必须调用new-handler函数;必须有应对零内存需求的准备;还需要避免不慎掩盖正常形式的new。
  operator new的返回值看上去非常单纯,如果它有能力供应客户申请的内存,就返回指针指向那块内存,如果没有能力,就遵循本文的第一个条款描述的内容,并抛出一个bad_alloc异常。
  然而其实也不是很单纯,因为operator new实际上并不只尝试分配一次内存,并在每次失败后调用new-handler函数。假设new-handler函数也许能做一些动作将某些内存释放出来。只有当指向new-handler函数的指针是null的时候,operator new才返回异常。
  C++规定,即使客户要求分配0bytes,operator new也得返回一个合法的指针,一个小伎俩是将申请0byte视为申请1byte。以下是一个non-member operator new的伪码:

void* operator new(std::size_t size) throw(std::bad_alloc){
	using namespace std;
	if(size == 0){
		size = 1;
	}
	while(true){
		尝试分配size bytes;
		if(分配成功)
		return (一个指针,指向分配来的内存);

		//分配失败,找出目前的new-handler函数
		new_handler globalHandler = set_new_handler(0);
		set_new_handler(globalHandler);

		if(globalHandler) 
			(*globalHandler)();
		else 
			throw std::bad_alloc();
	}
}

. 这段伪码中将new-handler函数指针设为null后又恢复原样,其实是因为没有办法能直接取得当前的new-handler指针(你应该还记得set_new_handler是返回之前指向的new-handler函数的指针吧)。
  operator new成员函数是会被派生类继承的,这会导致一些有趣的复杂度,如先前所说,写出定制版的operator new是为了某特定的class,而不是为了它的派生类,然而一旦被继承下去,可能基类的operator new被调用用来分配派生类的对象,处理这种行为的最佳做法是将“内存申请错误”的调用行为改为采用标准的operator new:

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

. 如果打算控制class专属之“arrays内存分配行为”,那么需要实现operator new[]。这个函数通常被称为“array new”。对此,唯一需要做的事就是分配一块未加工的内存,因为你无法对array之内迄今为止尚未存在的元素对象做任何事。实际上甚至无法计算这个array将含有多少个元素,首先你不知道每个对象多大,毕竟基类的operator new[]可能经由继承被调用,将内存分配给“元素为派生类对象”的array使用,而派生类对象通常比基类对象大。
  因此不能在Base::operator new[]内假设每个元素对象的大小是sizeof(Base),此外传递给operator new[]的size_t参数,其值有可能比“将被填以对象”的内存数量更多,因为动态分配的arrays有可能含有额外的空间来存放元素个数。
  以上是撰写operator new时需要注意的规则。对operator delete来说情况更加简单,唯一需要记住的事情就是C++保证“删除null指针永远安全”,所以在编写时要兑现这一规则,以下是operator delete的伪码:

void operator delete(void* pMem) throw(){
	if(pMem == 0) return;				//如果将被删除的是null指针,就啥也不做
	现在,归还pMem所指的内存;
}

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

class Base{
public:
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	static void operator delete(void* pMem,std::size_t size) throw();
	...
};
void Base::operator delete(void* pMem,std::size_t size) throw(){
	if(pMem == 0) return;
	if(size != sizeof(Base)){
		::operator delete(pMem);
		return;
	}
}

写了placement new也要写 placement delete

. 当写一个new表达式像这样:Widget* pw = new Widget;共有两个函数被调用:一个是用以分配内存的operator new,一个是Widget的默认构造函数。
  假设第一个函数调用成功,第二个函数却抛出异常。那么步骤一的内存分配所得必须取消并恢复原样,否则会造成内存泄露。在这个时候,客户没有能力去归还内存,因为如果Widget的构造函数抛出异常,pw尚未被赋值,客户手上也就没有指向该被归还的内存。取消步骤一的内存分配所得并恢复原样的责任因此落到C++运行期系统身上。
  运行期系统会调用步骤一所调用的operator new的相应的operator delete版本,前提是它知道哪个版本该被调用,如果面对的是正常签名式的new和delete,这并不是问题,因为正常的operator new对应正常的operator 的delete:

void* operator new(std::size_t size) throw(std::bad_alloc);
void operator delete(void* pMem) throw();			//global作用域中的正常签名式
void operator delete(void* pMem,std::size_t size) throw();	//class作用域中的正常签名式

. 因此,当只是用正常形式的new和delete,运行期系统毫无问题的可以找出那个“知道如何取消new所作所为并恢复原状”的delete,但是当声明非正常形式的operator new,就不知道如何挑选delete版本了。比如,假设有一个class专属的operator new,要求接受一个ostream,用来打日志,同时又写了一个正常形式的operator delete:

class Widget{
public:
	...
	static void* operator new(std::size_t size,std::ostream& logStream) 
		throw(std::bad_alloc);						//非正常形式的new
	static void operator delete(void* pMem,std::size_t size) throw();
													//正常形式的delete
};

//考虑到如下调用
Widget* pw = new(std::cerr) Widget;			

. 以上的客户调用将会在Widget的构造函数抛出异常的时候导致内存泄露,因为在内存分配成功后,而Widget的构造函数抛出异常,运行期系统有责任取消operator new的内存分配并恢复原状,然而运行期系统无法知道真正被调用的那个operator new是如何运作的,因此它无法取消内存分配并恢复原状。因此需要提供与调用的operator new相同的参数个数与类型的operator delete版本,否则将没有任何operator delete被调用:

void operator delete(void* pMem,std::ostream& ) throw();

. 那如果提供了对应版本的operator delete之后,并在代码中使用了delete,比如:delete pw;会发生什么事呢?其实它会调用正常的operator delete。这意味着如果要避免placement new带来的内存泄露麻烦,我们必须同时提供一个正常版本的operator delete(用于构造期间无异常抛出)和一个placement版本(用于构造期间有异常抛出)。后者的额外参数必须和placement new的一样。
  顺带一提的是,由于成员函数的名称会掩盖其外围作用域中的相同名称,比如类中的专有new版本会遮掩外部全局中的new版本,派生类的new版本会遮掩全局和基类的new版本。有一个可行的办法是:建立一个基类,内含所有的正常形式的new和delete版本,凡是想用自定义形式括充标准形式的客户,可利用继承机制和using声明式取得标准形式

猜你喜欢

转载自blog.csdn.net/rest_in_peace/article/details/84872083