文章目录
- 一、了解new-handler的行为
- 1、new和malloc的对比
- 2、set_new_handler的使用
- 3、new-handler设计要求
- 4、提供自己的set_new_handler和operator new
- 5、请记住
- 二、了解new和delete的合理替换时机
- 1、替换编译器提供的operator new或operator delete
- 2、请记住
- 三、编写new和delete时需固守常规
- 1、operator new的要求
- 2、请记住
- 四、写了placement new 也要写 placement delete
- 1、placement new(带有额外参数的new)
- 2、名称遮蔽
- 3、请记住
一、了解new-handler的行为
1、new和malloc的对比
- new构造对象,malloc不会;
- new分配不出内存会抛异常,malloc返回NULL;
- new分配不出内存可以调用用户设置的new-handler,malloc没有。
namespace std{
typedef void (*new_handler)();
//返回旧的handler
new_handler set_new_handler(new_handler p) throw();
}
2、set_new_handler的使用
set_new_handler的参数是一个指针,指向operator new无法分配足够内存时需要被调用的函数,其返回值指向set_new_handler被调用前正在执行的那个函数。具体使用方法如下:
void outOfMem(){
cerr << “Unable to satisfy request for memory\n”;
abort();//使程序异常中止
}
int main(){
set_new_handler(outOfMem);
int *pBigDataArray = new int [10000000];
...
}
就本例而言,如果无法创建如此大的数组,则outofmem就会被调用,于是程序在发出信息后abort。当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
3、new-handler设计要求
一个设计良好的new-handler可以提供如下功能:
- 让更多内存可被调用:一种实现方法是程序一开始执行时就分配一大块内存,而后当new_handler第一次被调用时,将它释放给程序使用。
- 安装另一个new-handler:如果当前无法获取更多内存,但又明确知道其他某个new-handler有这个能力,就应该主动使用set-handler进行替换。
- 卸载new-handler:将nullptr传递给set-new-handler。如果new-handler为nullptr,operator new会在分配内存不成功时抛出异常。
- 抛出bad_alloc(或派生自bad_alloc)的异常:这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
- 不返回:直接调用abort或者exit。
4、提供自己的set_new_handler和operator new
// 资源管理类(RAII),在构造过程中获得一笔资源,并在析构过程中释还
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&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
template<typename T>
// "mixin"风格的base class,这种class用来允许derived class 继承单一特定能力
// 本例中是:设定class专属之new-handler的能力
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 oldHander = currentHandler;
currentHandler = p;
return oldHander;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler)); // 资源管理类记录原来的new_handler
// 如果成功执行,返回新分配内存块指针;并恢复上面资源管理类记录的new_handler;
// 如果执行失败,抛出一个bad_allo异常,恢复上面资源管理类记录的new_handler,传播异常
return ::operator new(size);
}
// static 成员必须在class 定义式之外被定义(除非它们是const 而且是整数型,条款02)
// 以下语句将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
//有了这个class template,为Widget添加set_new_handler支持能力就轻而易举了:
// 只要令Widget继承自NewHandlerSupport<Widget>就好,如下:
class Widget : public NewHandlerSupport<Widget>
{
... // 和先前一样,不必声明set_new_handler 或 operator new
};
为了返回系统默认的new_handler,我们在set_new_handler处理完之后,进行了旧handler的返回,同时在operator new的调用中进行了NewHandlerHolder的包装,这样在return之后,h会自动调用析构函数,恢复成默认的new_handler。
5、请记住
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
- Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。
二、了解new和delete的合理替换时机
1、替换编译器提供的operator new或operator delete
首先,之所以会有人想要替换编译器提供的operator new 或 operator delete,下面是三个常见理由:
- 用来检测运用上的错误。
- 为了强化效能。定制版性能性能通常会胜过缺省版本。
- 为了收集使用上的统计数据。定制版可以帮助我们收集动态内存的大小、分布、寿命、分配次序、是否随时间改变等等各种状态。
定制一个operator new 十分简单。举个例子,下面是个快速发展得出的初阶段global operator new,促进并协助检测“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块起点之前)。
许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如它可能会要求指针的地址必须是4倍数或doubles的地址必须是8倍数。如果没有奉行这个约束条件,可能导致运行期硬件异常。有些体系则宣称如果齐位条件满足便提供较佳效率。在我们这个主题中,齐位意义重大,因为C++要求所有operator news返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作,所以令operator new返回一个得自malloc的指针是安全的。然而上述operator new 中我并未返回一个得自malloc的指针,而是返回一个得自malloc且偏移一个int大小的指针。没人能够保证它的安全!像齐位这一类技术细节,正可以在那种“因其他纷扰因素而被程序员不断抛出异常”的内存管理器中区分出专业质量的管理器。写一个总是能够运作的内存管理器并不难,难的是它能够优良地运作。
很多时候没有必要编写自己的内存管理器。某些编译器已经在它们的内存管理函数中切换至调试状态和志记状态;许多平台上已有商业产品可以替代编译器自带的内存管理器;另一个选择是开发源码领域中的内存管理器。Boost程序库的Pool(内存池)就是这样一个分配器。
2、请记住
- 有许多理由需要写个自定的new 和 delete ,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
三、编写new和delete时需固守常规
1、operator new的要求
- 返回正确的值;
- 内存不足时必须调用new-handling函数;
- 考虑0内存需求;
- 避免掩盖global new(虽然这更偏近class接口要求)。
2、请记住
- operator new内部有一个死循环,并在其中尝试分配内存,如果它不能分配,则调用new-handler,同时它应该能够处理0bytes申请。class member版本则应该处理“比正确大小大或者小的申请”。
- operator delete应该在收到nullptr时不做任何事,class版本还应该处理“比正确大小大或者小的申请”。
四、写了placement new 也要写 placement delete
一般而言,new表达式形式大致如下:
Widget* pw = new Widget;
其中一共调用了两个函数:operator new以及Widget的默认构造函数。如果operator new被成功调用,但默认构造函数却抛出了异常,我们应该释放分配的内存并让它恢复原状。客户并没有这个能力,因为pw此时尚未被赋值,客户并不知道已分配的内存的地址,该任务需要C++运行期系统完成。
解决方法很简单:调用operator new所对应的那个operator delete。听起来容易,但如果我们曾经声明过带有附加参数的operator new,对应的delete就不好找了。
1、placement new(带有额外参数的new)
如果一个operator new除了size_t之外还接受其他参数,那它就是一个placement版本。众多placement版本中最受欢迎的是“接受一个指针指向对象被构造之处”:
void* operator new(size_t,void* pMemory) throw();
假设现有一个operator new接受一个ostream,用来记录信息,同时也应该具备一个非正常形式的class专属delete:
class Widget{
public:
//非正常形式new
static void* operator new(size_t size,ostream& log) throw(bad_alloc);
//非正常形式的delete(与new对应)
static void operator delete(void*,ostream&) throw();
}
2、名称遮蔽
成员函数的名称会掩盖其外部作用域的相同名称,如果你的base class中只声明了一个placement new,客户将无法使用正常形式的operator new,delete同理。与此类似的,derived class中的operator news会遮蔽base版本与继承而来的版本。为了避免名称遮蔽,建立一个base class,内含所有正常形式的new与delete:
class StandardNewDeleteForms{
public:
//normal
static void* operator new(size_t size) throw(bad_alloc){
return ::operator new(size);
}
static void operator delete(void* pMemory) throw(){
return ::operator delete(pMemory);
}
//placement
static void* operator new(size_t size,void* ptr) throw(){
return ::operator new(size,ptr);
}
static void* operator delete(void* pMemory,void* ptr) throw(){
return ::operator delete(pMemory,ptr);
}
//nothrow
static void* operator new(size_t size,const nothrow_t& nt) throw(){
return ::operator new(size,nt);
}
static void* operator delete(void* pMemory,const nothrow_t& nt) throw(){
return ::operator delete(pMemory);
}
};
3、请记住
- placement new和placement delete必须同时存在且匹配;
- 自定义版本不得遮蔽正常版本。