条款49:了解new-handler的行为

new异常会发生什么事?

在旧式的编译器中,operator new分配内存失败的时候,会返回一个null指针。而现在则是会抛出一个异常。

而在抛出这个异常之前,还会先调用一个客户指定的错误处理函数:所谓的new-handler: set_new_handler函数。 在标准程序库中有声明:

namespace std
{
typedef void (*new_handler)(); // 函数指针,返回为void,参数为空
new_handler set_new_handler(new_handler p) throw();
}

set_new_handler参数是一个函数指针,指向无法分配足够内存的时候所调用的函数。其返回值也是一个函数指针,指向此函数调用前的那个new-handler函数。

现在我们指定一个分配内存失败后的施行代码作为样例:

void OutOfMem() // 返回型为void,参数为空
{
std::cerr << "Unable to satisfy request for memory" << std::endl;
std::abort();
}

接着在main函数中:

int main()
{
std::set_new_handler(OutOfMem); // 相当于注册一个失败之后的执行函数
int *pBigData = new int[100000000L];
return 0;
}

在本例中,如果分配失败,那么就会调用OutOfMem,然后执行abort函数。

事实上,一个良好的new-handler函数应该会不断的调用,在内部处理会释放一些内存,然后再尝试可能能不能成功分配内存等等。作者总结了一系列设计良好的new-handler函数应该有的特点:

  • 让更多的内存可被使用。 实现的一个策略:一开始就分配一个大内存,第一次的new-handler被调用的时候让还给程序。
  • 安装另一个new-handler。 如果知道另一个new-handler能够获取足够的内存,那么就安装它吧。
  • 卸除new-handler。 将null指针传给set_new_handler,那么分配失败就会直接抛出异常。
  • 不返回。 通常调用exit或者abort。

现在我们来写一下基于Widget类的定制版本的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的函数呢?

先想一下static的特性,如果我们不声明我static会怎样?那就是说,这几个函数会成为Widget的成员函数,也就是说,只有在Widget成功被构造的时候,这些函数才会存在。但是我们的new是构造这个Widget的,怎么可能调用尚不存在的函数呢?

所以我们要声明为静态函数,调用这些静态函数在构造出Widget对象来。

再来看这几个静态成员函数的的实现。

std::new_handler Widget::currentHandler = 0;

当前错误处理函数要初始化为空,表示直接抛出异常,不作处理。

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

还记得我们在本条款开始的时候说的吗?set_new_handler函数就是将传入的错误处理函数设置为当前的错误处理函数,返回一个旧的出来。上面的代码就是一个标准版的set_new_handler。

现在我们的重点在于operator new函数,实际上我们只是在global的new上面做了一层封装,使得成为自己定制的new。简单来说,就是先注册一个属于Widget类的专属错误处理函数,然后调用全局operator new函数,但是结束后或者抛出异常后要注册回去。 关键是注册回去,这里采用条款13的RAII方法来管理:一旦析构就注册回去。

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

OK,现在我们就可以放心的写我们定制的new而不用担心内存泄漏的问题了。

void *Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::new(size);
}

这个函数的第一个语句做了两件事情:

(1) 把new失败时候错误函数设置为currentHandler。

(2) set_new_handler返回原本错误处理函数,这个函数将作为构造函数的参数传进去,保存下来,随后还要设置回去。

现在来看我们main函数中可能存在的调用方式:

void OutOfMem();    // 错误处理函数
Widget::set_new_handler(OutOfMem); // 设置为Widget的专属错误处理函数
Widget *pw1 = new Widget; // 内存分配失败会调用OutOfMem函数

这个条款的最后还介绍了nothrow的使用:如果我们想要在分配失败的时候返回一个空指针,而不是抛出异常, 那么可以这样:

Widget *pw2 = new(std::nothrow) Widget;

作者总结

set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。

Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常。

条款50:了解new和delete的合理替换时机

这条款就比较文字上的东西了,本人觉得还是需要在开发的时候考虑到各种的情形才能充分领悟。下面仅仅是把这些要点进行阐述:

  • 用来检测运用上的错误。
  • 为了强化效能。 编译器所提供给我们的new和delete使用太过于中庸,它必须考虑到大小内存的分配,考虑破碎问题等,当我们需要效率更高的时候可以不考虑这些。使用定制版本会更好。
  • 为了收集使用上的统计数据。
  • 为了检测运用错误。
  • 为了增加分配和归还的速度。 举个例子来说,编译器提供的是线程安全的版本,而我们是单线程,不存在不安全情况。自己定制的效率更高。
  • 为了降低缺省内存管理器带来的空间额外开销。 缺省的空间开销更大,它还需要一个额外的标记。
  • 为了将相关对象成簇集中。
  • 为了获得非传统行为。

作者总结

有许多理由需要写个自定的new和delete,包括改善性能、对heap运用错误进行调试,手机heap使用信息。

条款51:编写new和delete时需要固守常规

new要返回正确值

operator new的返回值应该很简单:如果成功分配了内存,返回一个指针指向它。如果失败了,就抛出一个bad_alloc的异常。

C++规定,如果客户要一个0byte,也要返回一个合法指针。所以对0要特殊处理。下面提供一个伪代码:

void* operator new(std::size_t size)
{
if(size == 0)
{
size = 1;
}
while(true)
{
// 尝试分配size bytes
if(分配成功)
{
return (一个指向分配内存的指针);
}
// 分配失败,找出目前的错误处理函数并处理
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)
{
(*globalHandler)();
}
else
{
throw std::bad_alloc();
}
}
}

上面令人费解的一段大概就是为什么要先把错误处理函数设置成一个空,然后再设置回去呢?只是为了取出错误处理函数,并且执行它。

设置一个While(true),通常说来,如果分配失败,会在这个循环内回收已经释放的内存,然后重新分配,如果失败就调用错误处理函数,如果成功就取地址。

继承体系下的new

看下面这段代码你就会清楚发现继承体系下的错误new:

class Base
{
public:
static void *operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derive : public Base
{
public:
....
};

紧接着执行:

Derive *p = new Derive; // 这里调用的是Base的new!

很显然,如果Derive中未声明一个自己的定制new,那它调用的将是继承而来的Base的new函数。通常来说,Base和Derive的内存大小都不相等,这样调用将会出错。

如果Derive没有自己定制,想要使用global的,那就应该改成这样:

void *Base::operator(std::size_t size) throw(std::bad_alloc)
{
if(size != sizeof(Base))
{
return ::operator(size));
}
... // 这里是处理Base类的new
}

delete的编写注意

delete就相对比较简单了。但是我们要保证,delete一个null也是有效的。另外,在继承体系下应该这么写:

void operator delete(void *rawMemory,std::size_t size) throw()
{
if(rawMemory = null)
return ;
if(size != sizeof(Base))
{
::operator delete(rawMemory);
return ;
}
// 归还内存等操作
}

作者总结

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

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

条款52:写了placement new也要写placement delete

什么样的叫做placement

简单说就是在operator new或者delete函数中,除了必须要有的参数之外还需要有的其它参数,这样的一个operator placement版本。

如我们典型版本的new和delete是这样的:

void *operator new(std::size_t size);
void *operator delete(void* rawMemory) throw(); //global作用域的典型声明
void *operator delete(void* rawMemory,std::size_t size); // class作用域的典型声明

比如在class中,不属于void*和size_t的就是一个额外的参数。

placement版本是为了防止内存泄漏的一种方式。

考虑以下代码:

Widget *pw = new Widget;

这里会调用一个new和一个Widget的默认构造函数。假设第一个函数调用成功,内存得以分配。第二个函数却失败了,那么pw就还没被赋值,客户端没有能力归还内存,也就会造成内存泄漏!

这样取消内存分配并且回复旧观的任务就落在了C++运行期系统上了。现在有一个问题,运行期要怎么知道调用对应的delete版本呢? 如果调用错误的delete版本将不会获得预期的结果。

让运行期系统找到正确的delete版本

在我们有了placement版本的new时,如果构造函数抛出了异常,运行期系统首先寻找的是“(额外)参数个数和类型都与operator new相同”的某个operator delete。如果找到就进行调用,如果没有,那么将不会调用任何的delete函数!!!

如果不调用delete,在这种情况下内存百分百会泄漏,因此,我们必须为我们的placement new声明定义对应的operator delete:

class Widget
{
public:
static void *operator new(std::size_t size,std::ostream &logStream) throw(std::bad_allc);
static void operator delete(void *pMemory) throw();
static void operator delete(void *pMemory,std::ostream& logStream) throw();
};

像这样的,最后一个delete函数就是一个对应的placement delete函数,它有一个额外参数logStream。

现在我们看调用:

Widget *pw = new(std::cerr) Widget;
delete pw;

这次我们就确保了内存不会被泄漏:

(1) new成功了并且构造函数也成功了,那么delete pw删除的就是正确的内存。

(2) new成功了但是构造函数却失败了,那么delete调用的是placement delete版本,运行期系统将正确处理。

作者总结

当你写一个placement operator new,请确定也写出了对应placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。

当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们正常的版本。

八、定制new和delete的更多相关文章

  1. 高效C++:定制new和delete

    内存的申请和释放,C++从语言级别提供了new和delete关键字,因此需要了解和熟悉其中的过程. 了解new-handler的行为 set_new_handler可以指定一个函数,当申请内存失败时调 ...

  2. Effective C++ —— 定制new和delete(八)

    STL容器所使用的heap内存是由容器所拥有的分配器对象管理,不是被new和delete直接管理.本章并不讨论STL分配器. 条款49 : 了解new-handler的行为 当operator new ...

  3. 【effective c++】定制new和delete

    条款49: 了解new-handler的行为 operator new 和 operator delete只适合用来分配单一对象.array所用的内存由operator new[]分配出来,并由ope ...

  4. Effective C++: 08定制new和delete

    49:了解new-handler的行为 当operator new无法满足某一内存分配需求时,它会抛出异常(以前会返回一个null).在抛出异常之前,它会调用一个客户指定的错误处理函数,也就是所谓的n ...

  5. 《Effective C++》定制new和delete:条款49-条款52

    条款49:了解new-handler的行为 当operator new无法分配出内存会抛出异常std::bad_alloc 抛出异常前会反复调用用户自定义的new-handler函数直至成功分配内存 ...

  6. MQTT的学习研究(八)基于HTTP DELETE MQTT 订阅消息服务端使用

    HTTP DELETE 订阅主题请求协议和响应协议http://publib.boulder.ibm.com/infocenter/wmqv7/v7r0/topic/com.ibm.mq.csqzau ...

  7. python学习(八)定制类和枚举

    `python`定制类主要是实现特定功能,通过在类中定义特定的函数完成特定的功能. class Student(object): def __init__(self, name): self.name ...

  8. C++ delete 和 delete []

    C++ delete 和 delete [] 简单结论: new delete new [] delete []   文章 : 对 delete [] 的声明 void operator delete ...

  9. c++中的new和delete

    对于计算机程序设计而言,变量和对象在内存中的分配都是编译器在编译程序时安排好的,这带来了极大的不便,如数组必须大开小用,指针必须指向一个已经存在的变量或对象.对于不能确定需要占用多少内存的情况,动态内 ...

随机推荐

  1. Java第六周课堂示例总结

    一.如果一个类中既有初始化块,又有构造方法,同时还设定了字段的初始值,谁说了算? public class InitializeBlockDemo { /** * @param args */ pub ...

  2. C++ Primer 回炉重铸(一)

    过去学C++语法都是用的这本C++Primer第五版 说实话,这本书应该是业界用的最多的一本类似于C++语法的百科全书了.. 但是感觉自己学了这么长时间的C++,语法层次还是不够牢固. 比如templ ...

  3. GitHub从小白到熟悉<三>

    上传文件

  4. BM求线性递推模板(杜教版)

    BM求线性递推模板(杜教版) BM求线性递推是最近了解到的一个黑科技 如果一个数列.其能够通过线性递推而来 例如使用矩阵快速幂优化的 DP 大概都可以丢进去 则使用 BM 即可得到任意 N 项的数列元 ...

  5. MySQL安装过程中遇到的错误代码为1045的解决方法

    mysql的安装包,及其图形化破解软件:https://pan.baidu.com/s/1PIzaEGpC9QEPUwZ8OowhCw 二级压缩包下边的 视图化管理软件:Navicat.exe   发 ...

  6. Django框架——基础之视图系统(View.py)

    Django框架之View.py(视图文件) 1. 视图简介 视图层是Django处理请求的核心代码层,我们大多数Python代码都集中在这一层面. 它对外接收用户请求,对内调度模型层和模版层,统合数 ...

  7. 25、Nginx常见典型故障

    1.为什么nginx里面有的是浏览器渲染出的页面,有的时候就变成下载文件? 这个一个取决于服务端nginx,一个取决于你浏览器.在Nginx服务端的配置文件目录下,有一个mime.types 文件,内 ...

  8. 15、Nginx动静分离实战

    1.Nginx动静分离基本概述 动静分离, 通过中间件将动静分离和静态请求进行分离. 那为什么要通过中间件将动态请求和静态请求进行分离? 减少不必要的请求消耗, 同时能减少请求的延时. 通过中间件将动 ...

  9. LeetCode OJ -- 无重复字符的最长子串

    给定一个字符串,找出不含有重复字符的 最长子串 的长度. 示例: 给定 "abcabcbb" ,没有重复字符的最长子串是 "abc" ,那么长度就是3. 给定  ...

  10. linux 下mysql忘记密码或者安装好linux后不知道mysql初始密码解决方案

    1.使用yum安装mysql后 2.初始密码在/var/log/mysqld.log这个文件里 3.输入命令:grep 'temporary password' /var/log/mysqld.log ...