Effective C++: 08定制new和delete
49:了解new-handler的行为
当operator new无法满足某一内存分配需求时,它会抛出异常(以前会返回一个null)。在抛出异常之前,它会调用一个客户指定的错误处理函数,也就是所谓的new-handler。
客户通过调用set_new_handler来设置new-handler:
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
set_new_handler返回之前设置的new_handler。
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。因此,一个设计良好的new-handler必须做以下事:
a:让更多内存可被使用,以便使operator new下一次分配内存能够成功。实现方法之一就是程序一开始就分配一大块内存,而后当new-handler第一次被调用时,将它们还给程序使用;
b:安装另一个new-handler:如果目前的new-handler无法获得更多内存,并且它直到另外哪个new-handler有此能力,则当前的new-handler可以安装那个new-handler以替换自己,下次当operator new调用new-handler时,就是调用最新的那个。
c:卸载new-handler,一旦没有设置new-handler,则operator new就会在无法分配内存时抛异常;
d:抛出bad_alloc异常;
e:不返回,直接调用abort或exit。
有时希望根据不同的class有不同的方式处理内存分配的情况,但是C++并不支持class专属之new-handler,但是C++支持class专属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;
}; std::new_handler Widget::currentHandler = ; std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
使用Widget的用户首先调用Widget::set_new_handler设置其专属new-handler。然后,在Widget的专属operator new中,调用std::set_new_handler,设置全局new-handler为Widget的专属new-handler,然后调用全局operator new,执行内存分配。分配失败时,全局operator new会调用Widget的专属new-handler。
如果全局operator new最终无法分配足够内存,会抛出一个异常,或者分配成功后,此时,必须恢复全局new-handler为原来的设置。为了实现这一点,可以使用资源管理对象的方法:
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh):handler(nh) {}
~NewHandlerHolder() { std::set_new_handler(handler); }
private:
std::new_handler handler; // remember it NewHandlerHolder(const NewHandlerHolder&); // prevent copying
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
有了资源管理类之后,Widget::operator new的代码如下:
void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
//安装Widget专属new-handler,并使用NewHandlerHolder管理原有全局new-handler
NewHandlerHolder h(std::set_new_handler(currentHandler)); //不管分配成功还是抛出异常,NewHandlerHolder的析构函数中会恢复全局new-handler
return ::operator new(size);
}
使用Widget的客户代码如下:
void outOfMem();
Widget::set_new_handler(outOfMem); // 设置Widget的专属new-handler Widget *pw1 = new Widget; //如果分配失败,调用outOfMem std::string *ps = new std::string; //如果分配失败,调用全局new-handler Widget::set_new_handler();
Widget *pw2 = new Widget; //分配失败,直接抛出异常
实现class专属new-handler不会因class不同而不同,所以可以建立起一个mixin风格的base class。但是因为涉及到static成员,为了使derived class获得不同的base class属性,这里可以使用template:
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 oldHandler;
} 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);
} // this initializes each currentHandler to null
template<typename T> std::new_handler NewHandlerSupport<T>::currentHandler = ;
有了这个模板之后,Widget可以直接继承它:
class Widget: public NewHandlerSupport<Widget> {
...
};
这样看起来会有些奇怪,而且NewHandlerSupport模板从未使用类型参数T。这种技巧的作用,只是为了保证:继承自NewHandlerSupport的每一个class,都具有不同的NewHandlerSupport属性,也就是static成员变量currentHandler。这种技术还有自己的名字:“怪异的循环模板模式”(curiously recurring template pattern, CRTP)。
直到1993年,C++还要求operator new在分配失败时返回null,新一代的operator new则应该抛出bad_alloc异常,但是许多C++程序是在编译器支持新规范之前写出来的,因此C++提供了另一形式的operator new,他会在分配失败时返回null:
class Widget { ... };
Widget *pw1 = new Widget; //分配失败抛出bad_alloc异常
if (pw1 == ) ... // 这个测试一定失败 Widget *pw2 = new (std::nothrow) Widget; // 分配失败返回NULL
if (pw2 == ) ... // 这个测试可能成功
虽然nothrow版的operator new不抛异常,但是接下来Widget的构造函数调用时,内部可能又会new一些内存,而这次不一定会在使用nothrow new。所以,使用nothrow new只能保证operator new不抛异常,不保证像new (std::nothrow) Widget这样的表达式不抛异常,因此其实没有使用nothrow new的需要。
50:了解new和delete的合理替换时机
以下是替换编译器提供的operator new或operator delete的几个最常见的理由:
a:检测运用错误:内存相关的错误包括内存泄漏、多次delete等,使operator new持有一串地址,而operator delete将地址从中移走,可以很容易检测出上述错误用法。另外,编程错误可能导致数据写入点在分配区块之后(overruns)或之前(underrun),可以自定义一个operator new,超额分配内存,在额外空间放置特定的签名,operator delete可以检查签名是否正确来判断是否发生了overrun或underrun。
b:为了强化性能:编译器提供的operator new和operator delete主要用于一般目的,它们的工作对所有人都适度的好,但某些情况下,定制版的operator new和operator delete性能可以胜过缺省版本。要么是速度快,要么是更省内存。
c:为了收集使用数据;
d:为了弥补缺省分配器中的非最佳齐位,比如如果double是8-byte齐位的则访问速度最快,但编译器自带的operator new并不一定保证这一点,此时可以替换operator new为一个8-byte齐位的版本,使得程序效率大大提升;
e:为了将相关对象成簇几种:如果某种数据结构往往一起使用,你希望在处理这些数据时将内存页错误的频率降至最低,则new和delete的placement版本有可能完成这样的任务;
f:为了获得非传统的行为;
51:编写new和delete时需固守常规
编写operator new时需要注意:必得返回正确的值,内存不足时需要调用new-handling函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new;operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,则抛出一个bad alloc异常;operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。
下面是一个非成员函数的operator new伪码:
void * operator new(std::size_t size) throw(std::bad_alloc)
{ //你的operator new可能有更多的参数
using namespace std;
if (size == ) { //处理0内存需求,将其视为1Byte申请
size = ;
} while (true) {
attempt to allocate size bytes;
if (the allocation was successful)
return (a pointer to the memory); // 申请失败,找到当前的new-handler
new_handler globalHandler = set_new_handler();
set_new_handler(globalHandler); if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
因为没有办法可以直接取得当前的new-handler,所以先调用set_new_handler将其找出来。
对于operator new的成员函数版本,因为该函数会被derived classes继承,所以需要考虑的更周全些。因为写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何derived classes。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)的对象而设计。然而一旦被继承下去,有可能base class的operator new被调用用以分配derived class对象。处理这种情况的最佳做法是将内存申请量错误的调用行为改为标准operator new:
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base))
return ::operator new(size);
...
}
上面的代码并没有检测size等于0的情况,是因为C++保证所有独立式对象必须具有非0大小,所以sizeof(Base)不会等于0。
如果你打算控制class专属之“arrays内存分配行为”,那么你需要实现operator new [ ],唯一需要做的一件事就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array将含多少个元素对象。首先你不知道每个对象多大,毕竟base class的operator new [ ]有可能经由继承被调用,将内存分配给“元素为derived class对象”的array使用。此外,传递给operator new[]的size t参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的arrays可能包含额外空间用来存放元素个数。
operator delete情况更简单,需要记住的唯一事情就是C++保证“删除null指针永远安全”,下面是伪码:
void operator delete(void *rawMemory) throw()
{
if (rawMemory == ) return;
deallocate the memory pointed to by rawMemory;
}
这个函数的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 *rawMemory, std::size_t size) throw();
...
}; void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
if (rawMemory == ) return; if (size != sizeof(Base)) {
::operator delete(rawMemory);
return;
} deallocate the memory pointed to by rawMemory;
return;
}
有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t可能不正确。
52:写了placement new也要写placement delete
当你写一个new表达式: Widget* pw=new Widget; 这种情况下共有两个函数被调用,一个是用以分配内存的operator new,一个是Widget的default构造函数。
假设operator new调用成功,构造函数却抛出异常。这种情况下,内存分配所得必须取消并恢复旧观,否则会造成内存泄漏。回收内存的责任是由C++运行期系统完成的。
运行期系统会调用operator new的相应operator delete版本,前提是它必须知道哪一个(因为可能有许多个)operator delete该被调用。如果目前面对的是拥有正常签名式的new和delete,这并不是问题,因为正常的operator new对应于正常的operator delete:
void* operator new(std::size_t) throw(std::bad_alloc); void operator delete(void *rawMemory) throw(); // 全局域
void operator delete(void *rawMemory, std::size_t size) throw(); // 类专属
因此,当只使用正常形式的new和delete,运行期系统毫无问题可以找出那个“知道如何取消new所作所为并恢复旧观”的delete。然而当你开始声明非正常形式的operator new,也就是带有附加参数的operator new,“究竟哪一个delete伴随这个new”的问题便浮现了。
如果operatornew接受的参数除了size_t之外还有其他,这便是个所谓的placement new。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,那样的operator new长相如下:
void* operator new(std::size_t, void *pMemory) throw();
这个版本的new已被纳入C++标准程序库,只要#include <new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早的placement new版本。实际上它正是这个函数的命名根据:一个特定位置上的new。以上说明意味术语placement new有多重定义。当人们谈到placement new,大多数时候他们谈的是此一特定版本,少数时候才是指接受任意额外实参之operator new。因此一般性术语“placement new”意味带任意额外参数的new,因为另一个术语“placement delete”直接派生自它,operator delete如果接受额外参数,便称为placement deletes。
假设写一个class专属的operator new,要求接受一个ostream,用来记录相关分配信息,同时又写了一个正常形式的class专属operator delete:
class Widget {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void *pMemory, std::size_t size) throw();
...
};
Widget *pw = new (std::cerr) Widget;
如果内存分配成功,而Widget构造函数抛出异常,运行期系统需要取消operator new的分配并恢复旧观。然而运行期系统无法知道真正被调用的那个operator new如何运作,因此运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete。所以对应的operator delete就应该是:
void operator delete(void *, std::ostream&) throw();
现在,既然Widget没有声明placement版本的operator delete,所以运行期系统不知道如何取消并恢复原先对placement new的调用。于是什么也不做,这就造成了内存泄漏。
规则很简单:如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。
因此,Widget有必要声明一个placement delete,对应于那个有志记功能的placement new:
class Widget {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void *pMemory) throw();
static void operator delete(void *pMemory, std::ostream& logStream) throw();
...
};
这种情况下,如果Widget *pw = new(std::cerr) Widget中构造函数抛出异常,则对应的placement delete就会被调用。
如果手动delete:delete pw; 这种情况下,调用的是正常形式的operator delete,而非其placement版本。placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会调用,而delete一个指针永远不会调用placement delete。这就表示必须提供一个正常的operator delete,以及一个对应placement new的placement delete版本。
另外,由于成员函数的名称会掩盖外围作用域中的相同名称,因此需要避免让class专属的new掩盖客户期望的其他new,比如:
class Base {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
...
}; Base *pb = new Base; // 错误,正常的new已经被掩盖了
Base *pb = new (std::cerr) Base; // 正确,调用Base的placement new
同样的,derived classes中的operator new会掩盖global版本和继承而来的operator new:
class Derived: public Base {
public:
...
static void* operator new(std::size_t size) // 重新定义正常的operator new
throw(std::bad_alloc);
...
};
Derived *pd = new (std::clog) Derived; // 错误,Base的placement new被掩盖了
Derived *pd = new Derived; // 正确,调用Derived的正常operator new
缺省情况下,C++在global作用域内提供以下形式的operator new:
void* operator new(std::size_t) throw(std::bad_alloc); // normal new
void* operator new(std::size_t, void*) throw(); // placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); // see Item 49
如果在class内声明任何operator new,它都会遮掩这些标准形式。可以建立一个base class,内含所有正常形式的new和delete,在需要时,可以继承并覆盖:
class StandardNewDeleteForms {
public:
// normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc)
{ return ::operator new(size); }
static void operator delete(void *pMemory) throw()
{ ::operator delete(pMemory); } // placement new/delete
static void* operator new(std::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 new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
{ return ::operator new(size, nt); }
static void operator delete(void *pMemory, const std::nothrow_t&) throw()
{ ::operator delete(pMemory); }
};
当想自定义operator new和delete时,可以利用继承和using:
class Widget: public StandardNewDeleteForms { // inherit std forms
public:
using StandardNewDeleteForms::operator new; // make those
using StandardNewDeleteForms::operator delete; // forms visible static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc); static void operator delete(void *pMemory, std::ostream& logStream)
throw();
...
};
Effective C++: 08定制new和delete的更多相关文章
- 【effective c++】定制new和delete
条款49: 了解new-handler的行为 operator new 和 operator delete只适合用来分配单一对象.array所用的内存由operator new[]分配出来,并由ope ...
- 《Effective C++》定制new和delete:条款49-条款52
条款49:了解new-handler的行为 当operator new无法分配出内存会抛出异常std::bad_alloc 抛出异常前会反复调用用户自定义的new-handler函数直至成功分配内存 ...
- 高效C++:定制new和delete
内存的申请和释放,C++从语言级别提供了new和delete关键字,因此需要了解和熟悉其中的过程. 了解new-handler的行为 set_new_handler可以指定一个函数,当申请内存失败时调 ...
- Effective C++ —— 定制new和delete(八)
STL容器所使用的heap内存是由容器所拥有的分配器对象管理,不是被new和delete直接管理.本章并不讨论STL分配器. 条款49 : 了解new-handler的行为 当operator new ...
- 八、定制new和delete
条款49:了解new-handler的行为 new异常会发生什么事? 在旧式的编译器中,operator new分配内存失败的时候,会返回一个null指针.而现在则是会抛出一个异常. 而在抛出这个异常 ...
- Effective Java 08 Obey the general contract when overriding equals
When it's the case that each instance of the class is equal to only itself. 1. Each instance of the ...
- Effective C++ .08 别让异常逃离析构函数
异常不怎么用,C++能自己控制析构过程,也就有这个要求了.容器不能完全析构其中的元素真是太危险了
- 《Effective C++》读书摘要
http://www.cnblogs.com/fanzhidongyzby/archive/2012/11/18/2775603.html 1.让自己习惯C++ 条款01:视C++为一个语言联邦 条款 ...
- C++学习书籍推荐《Effective C++ 第三版》下载
百度云及其他网盘下载地址:点我 编辑推荐 <Effective C++:改善程序与设计的55个具体做法(第3版)(中文版)(双色)>前两个版本抓住了全世界无数程序员的目光.原因十分明显:S ...
随机推荐
- D - Tree and Hamilton Path
题意 给一棵树,问一个排列,使得按顺序走过这些点的路径最长. N<=100000 解法 为了能让每条边被经过的次数达到上界, 我们首先找出重心, 然后容易得出一种排列方案,使得答案为以重心为根的 ...
- 简单的选项卡制作(原生JS)
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...
- HttpComponents了解
原文地址:http://blog.csdn.net/jdluojing/article/details/7300428 1 简介 超文本传输协议(http)是目前互联网上极其普遍的传输协议,它为构建功 ...
- numpy 常用工具函数 —— np.bincount/np.average
numpy 常用工具函数 —— np.bincount/np.average numpy 常用api(一) numpy 常用api(二) 一个函数提供 random_state 的关键字参数(keyw ...
- Luogu P2577 [ZJOI2005]午餐(dp)
P2577 [ZJOI2005]午餐 题面 题目描述 上午的训练结束了, \(THU \ ACM\) 小组集体去吃午餐,他们一行 \(N\) 人来到了著名的十食堂.这里有两个打饭的窗口,每个窗口同一时 ...
- promise基础和进阶
本文不对Promise的做过深的解析,只对基础的使用方法,然后会记录一些promise的使用技巧,可以巧妙的解决异步的常见问题. 在过去一直理解的是解决了一直异步回调的坑,但是用了npm async之 ...
- 读书笔记--Hibernate in Action 目录
1.理解对象/关系持久化 2.启动项目 3.领域模型和元数据 4.映射持久化类 5.继承和定制类型 6.映射集合和实体关联 7.高级实体关联映射 8.遗留数据库和定制SQL 9.使用对象 10.事务和 ...
- opencv4 java 验证码噪点 8邻域降噪
工程下载地址https://download.csdn.net/download/qq_16596909/11503962 程序运行后,同样会把图片存放在以下路径 首先来看一下原图 二值化后,可以把这 ...
- Git同账号多平台配置
最近工作中使用到了Git,虽然以前学习过,但是已经忘的差不多了,遂将本次配置过程整理成笔记以备忘 生成公钥 ssh-keygen -t rsa -C "gana10007@163.com&q ...
- mysql优化-数据库设计基本原则
mysql优化-数据库设计基本原则 一.数据库设计三范式 第一范式:字段具有原子性 原子性是指数据库的所有字段都不可被再次划分,如下表就不满足原子性,起点与终点 字段就可被拆分为起点与终点两个字段. ...