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

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

. C++允许手动的管理内存,这是双刃剑,你可以使程序更有效率,也可能面临维护程序带来的麻烦,所以了解C++内存管理的例程很是重要,其中的两个主角是分配例程和归还例程(也就是operator new 和 operator delete),配角是new-handler,这是当operator new无法满足客户的内存需求时调用的函数。
  额外有一点要注意的是:STL容器所使用的heap内存是由容器所拥有的分配器对象管理,不是被new和delete直接管理。这里就不讨论STL分配器了,这部分内容在C++ primer中有提到:传送门

了解new-handler的行为

. 当operator new无法满足某一内存分配需求时,它会抛出异常。以前是返回一个null指针,但这条条款最后在讨论这个。
  当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler(其实operator new真正做到事稍微更复杂一点),为了指定这个new-handler函数,客户必须调用set-new-handler,那是声明于<new>的标准程序库函数:

namespace std{
	typedef void (*new_handler)();				
	new_handler set_new_handler(new_handler p) throw();
};

. 如上所述,new_handler是一个typedef(ps:其实我不太懂为啥要在typedef里用指针啊、括号啊什么的,感觉不好阅读),定义出一个指针指向一个函数,该函数没有参数也不返回任何东西。set_new_handler声明式尾部的throw()表示该函数不抛出任何异常,其参数是一个指针,指向当operator new无法分配足够内存时需要调用的函数,返回值也是一个指针,指向之前的new_handler函数。可以如下使用:

void outOfMem(){
	std::cerr<<"out of memory\n";
	std::abort();
}
int main(){
	std::set_new_handler(outOfMem);
	int* pBigDataArray = new int[100000000000000000];
	...
}

. 如果operator new 无法分配那么多的内存,就会调用设置好的outOfMem,于是程序在发出一个信息后夭折。
  当operator new无法满足内存申请时,它会不断调用new-handler函数,知道找到足够的内存。一个设计良好的new-handler函数必须做以下事情:
  1.让更多内存可被使用。这便造成operator new的下一次内存分配动作可能完成。实现这一策略的一种方法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。
  2.安装另一个new-handler。如果目前这个new-handler无法获得更多的可用内存,或许它知道另外那个new-handler有这个能力。果真如此,目前这个new-handler就可以安装另外的new-handler以替换自己。
  3.删除new-handler。也就是传递null给set-new-handler。在没有安装new-handler的情况下,operator new会在内存分配不足时抛出异常。
  4.抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
  5.不返回。通常调用abort或exit。
  这样的弹性为你在实现new-handler函数时拥有更多选择。
  有时或许希望以不同的方式处理内存分配失败的情况,比如每个类中有不同的outOfMem函数,在属于该类的分配物出现分配不成功时调用该类的outOfMem,只需令每一个class提供自己的set_new_handler和operator new即可。例如:

class Widget{
public:
	static std::new_handler set_new_handler(std::new_handler p) throw();
	static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
	static std::new_handler currentHandler;
};
//static成员需要在类外定义(除非它们是const且是整数型)
std::new_handler Widget::currentHandler = 0;				//初始化为null

std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
	std::new_handler oldHandler = currentHandler;
	currentHandler = p;
	return oldHandler;
}

. 最后operator new需要做的事情就是:
  1.调用set_new_handler,安装widget的new-handler;
  2.调用operator new,执行实际的内存分配,如果分配失败,调用widget的new-handler,如果最终还是不能分配成功,会抛出一个bad_alloc异常,在此情况下Widget的operator new必须回复原本的new-handler函数,然后在传播这异常。为确保原本的new-handler能被重新安装回去,可以运用资源管理对象防止资源泄露
  3.如果operator new能够分配足够一个Widget对象所使用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget的析构函数会管理new-handler,自动将安装之前的那个new-handler恢复回来。
  从代码在阐述一次:

class NewHandlerHolder{
public:
	explicit NewHandlerHolder(std::new_handler nh)
		:handler(nh) {}
	~NewHandlerHolder(){
		std::set_new_handler(handler);
	}
private:
	std::new_handler handler;
	NewHandlerHolder(const NewHandlerHolder&);		//阻止copying
	NewHandlerHolder& operator=(const NewHandlerHolder&);
};

//这使得operator new的实现变得简单
void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
	NewHandlerHolder h(std::set_new_handler(currentHandler));		
	return ::operator new(size);							//分配内存或抛出异常
}									//函数结束的时候恢复原来的new-handler

. 可以使用模板来少写一些代码:

template <typename T>
class NewHandlerSupport{
public:
	static std::new_handler set_new_handler(std::new_handler p) throw();
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	...
private:
	static std::new_handler currentHandler;
};
template <typename T>
std::new_handler 
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){
	std::new_handler oldHandler = currentHandler;
	currentHandler = p;
	return currentHandler;
}
template <typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) 
throw(std::bad_alloc){
	NewHandlerHolder h(std::set_new_handler(currentHandler));		
	return ::operator new(size);							
}	

//初始化每个currenHandler
template <typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

. 有了这个模板类,为Widget添加set_new_handler支持能力就轻而易举了:只要令Widget继承自NewHandlerSupport<Widget>就好。例如:

class Widget : public NewHandlerSupport<Widget>{
	...
}

. 新一代的operator new在无法分配足够内存的时候会抛出bad_alloc异常,但很多C++程序是在编译器开始支持新规范前写出来的。C++并不像抛弃那些侦测null的族群,于是提供另一个形式的operator new,这个形式被称为“nothrow”形式:

Widget* p = new(std::nothrow) Widget;
if(p == 0){
	...
}

. 不过nothrow new对异常的强制保证性不高,因为如果这样使用的时候,内存分配失败的话会返回null,但是如果分配成功的话,Widget的构造函数将被调用,在构造函数中可能有出现分配内存的行为,这里出现错误的时候将不会再强制使用nothrow new,这就可能抛出异常了。

了解new和delete的合理替换时机

. 对于想要替换编译器提供的operator new和operator delete的行为,一般有下面三种原因:
  1.用来检测运用上的错误。如果将new分配的内存delete失败的话将导致内存泄露,自己编写的时候可以进行检测是否释放成功或者记录下日志;
  2.为了强化效能。编译器所带的operator new和operator delete主要用于一般的目的。它们的工作对每个人都是适度的好,但不对特定任何人有最佳表现。通常定制版的operator new和operator delete性能更高;
  3.为了收集使用上的统计数据。在定制new和delete之前,应该先收集你的软件如何使用动态内存。比如分配区块的大小分布如何?寿命分布如何?倾向于使用FIFO(先进先出)次序还是LIFO(后进先出)次序或是随机次序?等等信息,自定义operator new和operator delete使我们得以轻松收集这些信息。
  一个简单的定制型operator new,除了分配内存外还协助检测“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块起点之前),其中还有一些小错误:

static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
void* operator new(std::size_t size) throw(std::bad_alloc){
	using namespace std;
	size_t realSize = size + 2 * sizeof(int);	//增加大小,
											//使得能够塞入两个signature
	void* pMem = malloc(realSize);			//调用malloc分配内存
	if(!pMem) throw bad_alloc();

	//将signature写入最前和最后
	*(static_cast<int*>(pMem)) = signature;
	*(reinterpret_cast<int>(static_cast<int*>(pMem) 
		+ realSize - sizeof(int))) = signature;
	
	//返回指针,指向位于第一个signature之后的内存位置
	return static_cast<Byte*>(pMem) + sizeof(int);
}

. 这个operator new的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持C++规矩”的态度。比如所有的operator new都应该含有一个循环,不停的调用某个new-handler函数,这里却没有。但这里主要讨论另一个主题:齐位。
  许多计算机体系结构要求特定的类型必须放在特定的内存地址上,例如它可能要求指针的地址是4的倍数或double的地址是8的倍数。在目前的这个主题中,齐位意义重大,因为C++要求所有的operator new返回的指针都有适当的对齐(取决于数据类型)。如果返回一个得自malloc的指针是安全的,但是上述返回的是一个得自malloc且偏移一个int大小的指针,没人能保证它的安全。
  现在在总结一下使用定制的版本去替换缺省的版本的目的:
  1.为了检测运用错误。如前所述;
  2.为了收集动态分配内存的统计信息。如前所述;
  3.为了增加分配和归还的速度。泛用型分配器往往比特定型分配器慢,特别是当定制型分配器专门针对某特定类型的对象设计时。
  4.为了降低缺省内存管理器带来的空间额外开销。泛用型分配器往往还占用更多的内存,因为它们往往在每个分配区块身上招引某些额外开销。
  5.为了弥补缺省分配器的非最佳齐位。在X86体系结构上doubles的访问时最快的——如果它们都是8位齐位。但是编译器自带的operator new并不保证对动态分配而得的doubles采取8位齐位。这种情况下,将缺省的operator new替换为一个8位齐位的保证版本,可导致效率大幅提升。
  6.为了将相关对象成簇几种。如果知道某个数据结构常常被一起使用,而你又希望在处理这些数据的时候将“内存页错误”的频率降到最低,那么为这个数据结构创建一个heap就有意义,这么一来它们就可以被集中在尽可能少的内存页上。
  7.为了获得非传统的行为。可以做点别的编译器自带的operator new版本没有做到的事。

猜你喜欢

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