您的位置:首页 > 编程语言 > C语言/C++

effective C++: 8.定制new和delete

2013-12-12 20:04 316 查看
八、定制new和delete

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

当 operator new 不能满足一个内存分配请求时,它抛出一个 exception(异常)。很久以前,他返回一个 null pointer(空指针),而一些比较老的编

译器还在这样做。你依然能得到以前的行为(在一定程度上),但是我要到这个 Item 的最后再讨论它。

在 operator new 因回应一个无法满足的内存请求而抛出一个 exception 之前,它先调用一个可以由客户指定的被称为 new-handler 的 error-handling

function(错误处理函数)。(这并不完全确切,operator new 真正做的事情比这个稍微复杂一些,详细细节在 Item 51 提供。)为了指定 out-of-

memory-handling function,客户调用 set_new_handler ——一个在 <new> 中声明的标准库函数:

namespace std {

typedef void (*new_handler)();

new_handler set_new_handler(new_handler p) throw();

}

就像你能够看到的,new_handler 是一个指针的 typedef,这个指针指向不取得和返回任何东西的函数,而 set_new_handler 是一个取得和返回一个

new_handler 的函数。(set_new_handler 的声明的结尾处的 "throw()" 是一个 exception specification(异常规范)。它基本上是说这个函数不会抛

出任何异常,尽管真相更有趣一些。关于细节,参见 Item 29。)

set_new_handler 的形参是一个指向函数的指针,这个函数是 operator new 无法分配被请求的内存时应该调用的。set_new_handler 的返回值是一个指向

函数的指针,这个函数是 set_new_handler 被调用前有效的目标。

你可以像这样使用 set_new_handler:

// function to call if operator new can't allocate enough memory

void outOfMem()

{

std::cerr << "Unable to satisfy request for memory/n";

std::abort();

}

int main()

{

std::set_new_handler(outOfMem);

int *pBigDataArray = new int[100000000L];

...

}

如果 operator new 不能为 100,000,000 个整数分配空间,outOfMem 将被调用,而程序将在发出一个错误信息后中止。(顺便说一句,考虑如果在写这个

错误信息到 cerr... 的过程中内存必须被动态分配会发生什么。)

当 operator new 不能满足一个内存请求时,它反复调用 new-handler function 直到它能找到足够的内存。引起这些重复调用的代码在 Item 51 中展示

,但是从这种高层次的描述已足够推导出一个设计得好的 new-handler function 必须做到以下事情之一:

Make more memory available(使得更多的内存可用)。这可能使得 operator new 中下一次内存分配的尝试成功。实现这一策略的一个方法是在程序启动

时分配一大块内存,然后在 new-handler 第一次被调用时释放它供程序使用。

Install a different new-handler(安装一个不同的 new-handler)。如果当前的 new-handler 不能做到使更多的内存可用,或许它知道有一个不同的

new-handler 可以做到。如果是这样,当前的 new-handler 能在它自己的位置上安装另一个 new-handler(通过调用 set_new_handler)。operator new

下一次调用 new-handler function 时,它会得到最近安装的那一个。(这个主线上的一个变化是让一个 new-handler 改变它自己的行为,这样,下一次

它被调用时,可以做一些不同的事情。做到这一点的一个方法是让 new-handler 改变能影响 new-handler 行为的 static(静态),namespace-specific

(名字空间专用)或 global(全局)的数据。)

Deinstall the new-handler(卸载 new-handler),也就是,将空指针传给 set_new_handler。没有 new-handler 被安装,当内存分配没有成功时,

operator new 抛出一个异常。

Throw an exception(抛出一个异常),类型为 bad_alloc 或继承自 bad_alloc 的其它类型。这样的异常不会被 operator new 捕获,所以它们将被传播

到发出内存请求的地方。

Not return(不再返回),典型情况下,调用 abort 或 exit。

这些选择使你在实现 new-handler functions 时拥有极大的弹性。

有时你可能希望根据被分配 object 的不同,用不同的方法处理内存分配的失败:

class X {

public:

static void outOfMemory();

...

};

class Y {

public:

static void outOfMemory();

...

};

X* p1 = new X; // if allocation is unsuccessful,

// call X::outOfMemory

Y* p2 = new Y; // if allocation is unsuccessful,

// call Y::outOfMemory

C++ 没有对 class-specific new-handlers 的支持,但是它也不需要。你可以自己实现这一行为。你只要让每一个 class 提供 set_new_handler 和

operator new 的它自己的版本即可。class 的 set_new_handler 允许客户为这个 class 指定 new-handler(正像standard set_new_handler 允许客户指

定global new-handler)。class 的 operator new 确保当为 class objects 分配内存时,class-specific new-handler 代替 global new-handler 被使

用。

假设你要为 Widget class 处理内存分配失败。你就必须清楚当 operator new 不能为一个 Widget object 分配足够的内存时所调用的函数,所以你需要

声明一个 new_handler 类型的 static member(静态成员)指向这个 class 的 new-handler function。Widget 看起来就像这样:

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 class members(静态类成员)必须在 class 定义外被定义(除非它们是 const 而且是 integral ——参见 Item 2),所以:

std::new_handler Widget::currentHandler = 0; // init to null in the class

// impl. file

Widget 中的 set_new_handler 函数会保存传递给它的任何指针,而且会返回前次调用时被保存的任何指针,这也正是 set_new_handler 的标准版本所做

的事情:

std::new_handler Widget::set_new_handler(std::new_handler p) throw()

{

std::new_handler oldHandler = currentHandler;

currentHandler = p;

return oldHandler;

}

最终,Widget 的 operator new 将做下面这些事情:

以 Widget 的 error-handling function 为参数调用 standard set_new_handler。这样将 Widget 的new-handler 安装为 global new-handler。

调用 global operator new 进行真正的内存分配。如果分配失败,global operator new 调用 Widget 的 new-handler,因为那个函数刚才被安装为

global new-handler。如果 global operator new 最后还是无法分配内存,它会抛出一个 bad_alloc exception。在此情况下,Widget 的 operator new

必须恢复原来的 global new-handler,然后传播那个 exception。为了确保原来的 new-handler 总能被恢复,Widget 将 global new-handler 作为一种

资源对待,并遵循 Item 13 的建议,使用 resource-managing objects(资源管理对象)来预防 resource leaks(资源泄漏)。

如果 global operator new 能够为一个 Widget object 分配足够的内存,Widget 的 operator new 返回一个指向被分配内存的指针。object 的用于管理

global new-handler 的 destructor(析构函数)自动将 global new-handler 恢复到调用 Widget 的 operator new 之前的状态。

以下就是你如何在 C++ 中表达这所有的事情。我们以 resource-handling class 开始,组成部分中除了基本的 RAII 操作(在构造过程中获得资源并在析

构过程中释放)(参见 Item 13),没有更多的东西:

class NewHandlerHolder {

public:

explicit NewHandlerHolder(std::new_handler nh) // acquire current

:handler(nh) {} // new-handler

~NewHandlerHolder() // release it

{ std::set_new_handler(handler); }

private:

std::new_handler handler; // remember it

NewHandlerHolder(const NewHandlerHolder&); // prevent copying

NewHandlerHolder& // (see Item 14)

operator=(const NewHandlerHolder&);

};

这使得 Widget 的 operator new 的实现非常简单:

void * Widget::operator new(std::size_t size) throw(std::bad_alloc)

{

NewHandlerHolder // install Widget's

h(std::set_new_handler(currentHandler)); // new-handler

return ::operator new(size); // allocate memory

// or throw

} // restore global

// new-handler

Widget 的客户像这样使用它的 new-handling capabilities(处理 new 的能力):

void outOfMem(); // decl. of func. to call if mem. alloc.

// for Widget objects fails

Widget::set_new_handler(outOfMem); // set outOfMem as Widget's

// new-handling function

Widget *pw1 = new Widget; // if memory allocation

// fails, call outOfMem

std::string *ps = new std::string; // if memory allocation fails,

// call the global new-handling

// function (if there is one)

Widget::set_new_handler(0); // set the Widget-specific

// new-handling function to

// nothing (i.e., null)

Widget *pw2 = new Widget; // if mem. alloc. fails, throw an

// exception immediately. (There is

// no new- handling function for

// class Widget.)

无论 class 是什么,实现这个方案的代码都是一样的,所以在其它地方重用它就是一个合理的目标。使它成为可能的一个简单方法是创建一个 "mixin-

style" base class(“混合风格”基类),也就是说,一个设计为允许 derived classes(派生类)继承一个单一特定能力(在当前情况下,就是设定一

个 class-specific new-handler 的能力)的 base class(基类)。然后把这个 base class(基类)转化为一个 template(模板),以便于你得到针对

每一个 inheriting class(继承来的类)的 class data 的不同拷贝。

这个设计的 base class(基类)部分让 derived classes(派生类)继承它们全都需要的 set_new_handler 和 operator new functions,而这个设计

template(模板)部分确保每一个 inheriting class(继承来的类)得到一个不同的 currentHandler data member(数据成员)。这听起来可能有点复杂

,但是代码看上去可靠而且熟悉。实际上,仅有的真正不同是它现在可以用在任何需要它的 class 之上:

template<typename T> // "mixin-style" base class for

class NewHandlerSupport{ // class-specific set_new_handler

public: // support

static std::new_handler set_new_handler(std::new_handler p) throw();

static void * operator new(std::size_t size) throw(std::bad_alloc);

... // other versions of op. new —

// see Item 52

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 = 0;

有了这个 class template(类模板),为 Widget 增加 set_new_handler 支持就很容易了:Widget 只需要从 NewHandlerSupport<Widget> 继承即可。(

可能看起来很奇特,但是下面我将解释更多的细节。)

class Widget: public NewHandlerSupport<Widget> {

... // as before, but without declarations for

}; // set_new_handler or operator new

这些就是 Widget 为了提供一个 class-specific set_new_handler 所需要做的全部。

但是也许你依然在为 Widget 从 NewHandlerSupport<Widget> 继承而烦恼。如果是这样,当你注意到 NewHandlerSupport template 从来没有用到它的

type parameter T 时,你可能会更加烦恼。它不需要那样做。我们需要的全部就是为每一个从 NewHandlerSupport 继承的 class 提供一份不同的

NewHandlerSupport ——特别是它的 static data member(静态数据成员)currentHandler ——的拷贝。template parameter T 只是为了将一个

inheriting class 同另一个区分开来。template 机制自己自动地为每一个被实例化的 NewHandlerSupport 中的 T 生成一个 currentHandler 的拷贝。

对于 Widget 从一个把 Widget 当作一个 type parameter(类型参数)的 templatized base class(模板化基类)继承,如果这个概念把你弄得有点糊涂

,不必难受。它最开始对每一个人都有这种影响。然而,它发展成如此有用的一项技术,它有一个名字,虽然它正常看上去所反映的事实并不是他们第一次

看到它的样子。它被称作 curiously recurring template pattern(奇特的递归模板模式) (CRTP)。真的。

在这一点上,我发表了一篇文章建议一个更好的名字叫做 "Do It For Me",因为当 Widget 从 NewHandlerSupport<Widget> 继承时,它其实是在说:“我

是 Widget,而我要从针对 Widget 的 NewHandlerSupport class 继承。”没有人使用我提议的名字(甚至是我自己),但是把 CRTP 考虑成说 "do it

for me" 的一种方式也许会帮助你理解 templatized inheritance(模板化继承)在做些什么。

像 NewHandlerSupport 这样的 templates 使得为任何有需要的 class 添加一个 class-specific new-handler 变得易如反掌。然而,mixin-style

inheritance(混合风格继承)总是会导致 multiple inheritance(多继承)的话题,而在我们沿着这条路走下去之前,你需要阅读 Item 40。

直到 1993 年,C++ 还要求 operator new 不能分配被请求的内存时要返回 null。operator new 现在则被指定抛出一个 bad_alloc exception,但是很多

C++ 程序是在编译器开始支持这个修订标准之前写成的。C++ 标准化委员会不想遗弃这些 test-for-null(检验是否为 null)的代码基础,所以他们提供

了 operator new 的另一种可选形式,用以提供传统的 failure-yields-null(失败导致 null)的行为。这些形式被称为 "nothrow" 形式,这在一定程度

上是因为它们在使用 new 的地方使用了 nothrow objects(定义在头文件 <new> 中):

class Widget { ... };

Widget *pw1 = new Widget; // throws bad_alloc if

// allocation fails

if (pw1 == 0) ... // this test must fail

Widget *pw2 =new (std::nothrow) Widget; // returns 0 if allocation for

// the Widget fails

if (pw2 == 0) ... // this test may succeed

对于异常,nothrow new 提供了比最初看上去更少的强制保证。在表达式 "new (std::nothrow) Widget" 中,发生了两件事。首先,operator new 的

nothrow 版本被调用来为一个 Widget object 分配足够的内存。如果这个分配失败,众所周知,operator new 返回 null pointer。然而,如果它成功了

,Widget constructor 被调用,而在此刻,所有打的赌都失效了。Widget constructor 能做任何它想做的事。它可能自己 new 出来一些内存,而如果它

这样做了,它并没有被强迫使用 nothrow new。那么,虽然在 "new (std::nothrow) Widget" 中调用的 operator new 不会抛出,Widget constructor 却

可以。如果它这样做了,exception 像往常一样被传播。结论?使用 nothrow new 只能保证 operator new 不会抛出,不能保证一个像 "new

(std::nothrow) Widget" 这样的表达式绝不会导致一个 exception。在所有的可能性中,你最好绝不需要 nothrow new。

无论你是使用 "normal"(也就是说,exception-throwing)new,还是它的稍微有些发育不良的nothrow,理解 new-handler 的行为是很重要的,因为它可

以用于两种形式。

请记住:

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

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

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

怎么会有人想要替换编译器提供的operator new或operator delete呢?我们可以列出如下三个常见的理由:

■ 用来检测运用上的错误.

程序员从开始编写代码到调试直至最终完成,这一过程当中犯下各种各样的错误在所难免,这些错误就可能

导致内存泄露(memory leaks)、不确定行为产生、overruns(写入点在分配区块尾端之后)、underruns(写入点

在分配区块尾端之前)等不良结果的发生.如果我们自定义operator news,我们就可以超额分配内存,用额外空

间放置特定签名来检测类问题.

■ 为了强化效能.

我们所用的编译器中自带的operator new和operator delete主要是用于一般的目的能够为各种类型的程序

所接受,而不考虑特定的程序类型.它们必须处理一系列需求,必须接纳各种分配形态,必须要考虑破碎问题等等

这些问题,因此编译器所带的operator new和operator delete采取中庸之道也是没办法的事情.它们的工作对每

个人都是适度地好,但不对特定任何人有最佳表现.通常可以发现,定制版之operator new和operator delete性

能胜过缺省版本.所谓的'胜过',就是它们比较快,有时甚至快很多,而且它们需要内存比较少,最高可省50%,所

以说对某些运用程序而言,将缺省new和delete替换为定制版本,是获得重大效能提升的办法之一.

■ 为了收集使用上的统计数据.

收集你的软件如何使用其动态内存.分配区块的大小发布如何?寿命发布如何?它们倾向于以FIFO次序或LIFO

次序或随机次序来分配和归还?它们的运用形态是否随时间改变,也就是说你的软件在不同执行阶段有不同的分配

/归还形态吗?任何时刻所使用的最大动态内存分配量是多少?自行定义的operator new和operator delete使我们

得以轻松收集这些信息.

基于上述三种理由,我不得不开始了写一个定制型operator new了,我的初稿看起来如下述代码所示,其中还存

在不少小错误,稍后我会完善它.

typedef const int signature = 0xDEADBEEF;

typedef unsigned char Byte;

void* operator new( std::size_t size)throw(std::bad_alloc)

{

using namespace std;

size_t real_size = size + 2 * sizeof(int);



void* applied_memory = malloc( real_size );

if( applied_memory == 0 ){

throw bad_alloc();

}

//将signature写入内存的最前段落和最后段落.

*(static_cast<int*>( applied_memory ) = signature;

*(reinterpret_cast<int*>(static_cast<Byte*>( applied_memory )

+ real_size - sizeof(int) ) ) = signature;



//返回指针,指向恰位于第一个signature之后的内存位置.

return static_cast<Byte*>( applied_memory ) + sizeof(int);

}

我刚才说了,这个版本有不少问题.而现在我只想专注一个比较微妙的主题:齐位(alignment).关于齐位的具

体是什么? 我假设大家都已经知道了,我在这里就不唠叨讲了.因为C++要求所有operator news返回的指针都有

适当的对齐(取决于数据类型).malloc就是在这样的要求下工作的,所以令operator new返回一个得自malloc的指

针是安全地.然而上述operator new中我并未返回一个得自malloc的指针,而是返回一个得自malloc且偏移一个

int大小的指针.没有人能够保证它的安全!我们可能因使用该版本的operator new导致获得一个未有适当齐位的

指针.那可能会造成程序崩溃或执行速度变慢.不论那种情况都不是我们希望看到的结果.

写一个总是能够运行的内存管理器并不难,难的是它能够优良地运作.一般而言,本书的作者建议你在必要的

稍后才试着写写看.很多时候这是非必要的.某些编译器已经在它们的内存管理函数中切换至调试状态和志记状态

.快速浏览一下你的编译器文档,很可能就消除了你自行写new和delete的需要了.

另一个选择是开放源码领域中的内存管理器.它们对许多平台都可用,你可以下载试试.Boost程序库的Pool就

是这样一个分配器,它对于常见的'分配大量小型对象'很有帮助.TR1支持各种类型特定对齐条件,很值得注意.

讨论到这里,我们又可以为本款开头讨论的问题理由再添加几条了,呵呵:

■ 为了增加分配和归还的速度.

泛用型分配器往往比定制型分配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时.

■ 为减低缺省内存管理器带来的空间额外开销.

泛用型内存管理器往往还使用更多的内存,那是因为它们往往常常在每一个分配区块身上招引某些额外开销.

■ 为了弥补缺省分配器中的非最佳齐位.

■ 为了将相关对象成簇集中(详略).

■ 为了获得非传统行为(详略).

OK,our topic talk is over!

请记住:

有许多理由需要写个自定义的new和delete,包括改善效能,对heap运用错误进行调试,收集heap使用信息.

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

前一条款我们已经讨论了你在什么时候想要写个自定义的operator new和operator delete,但并没有解释当你这么做时必须遵守什么规则.我先来总体说一

下,然后再分析这些规则.

要实现一致性operator new必得返回正确的值,内存不足时必得调用new-handling函数(见条款49),必须有对付零内存需求的准备,还需避免不慎掩盖正

常形式的new(虽然这比较偏近class的接口要求而非实现要求).

先来说说关于其返回值,如果它有能力供应客户申请的内存,就返回一个指针指向那块内存.如果没有那个能力,就遵循49描述的原则,并抛出一bad_alloc

异常.

而实际上operator new不止一次尝试进行内存分配,并在每次失败后调用new-handing函数.这里假设new-handling函数也许能够做某些动作释放某些内

存.只有当当前的new-handling函数指针为null时,operator new才会抛出异常.

C++规定,即使客户要求0bytes,operator new也得返回一个合法指针.这种诡异的行为其实是为了简化语言的其它部分.下面是个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 ){

...//try to allocate size bytes memory.

if( allocate_succeed ){

return (point_to_allocted_memory);

}

//allocate failed:find current new-handling function(as following)

new_handler global_handler = set_new_handler( 0 );

set_new_handler( global_handler );



if( global_handler ){

( *global_handler )();

} else {

throw std::bad_alloc();

}

}

}

现在我们注意一下这里的一个可能会出现的问题:很多人没有意识到operator new成员函数会被derived classes继承,那就会出现,有可能base class的

operator new被调用用以分配derived class对象:

struct Base{

static void* operator new(std::size_t size) throw( std::bad_alloc );

...

};

struct Derived:public Base{...};

Derived* p = new Derived;//call Base::operator new.

如果Base 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 ); //call standard operator new version.

}

...

}

如果你打算控制class专属之'arrays内存分配行为',那么你需要实现operator new的array兄弟版:operator new[].这个通常被称为"array new".如果

你要写这个operator new[],记住,唯一需要做的事情就是分配一块未加工内存.因为你无法对array之内迄今为止尚未存在的元素对象做任何事情.实际上你

甚至无法计算这个array将含有多少个元素.首先你不知道每个对象多大,因此你不能在Base::operator new[]内假设每个元素对象大小是sizeof(Base),此外

传递给它的参数size的值有可能比'将被填以对象'的内存数量更多.

这就是写operator new时候你需要奉行的规矩.operator delete情况更简单,你需要记住的唯一一件事情就是C++保证'删除null指针永远安全',所以你

必须兑现这项保证.下面就是non-member operator delete的伪码:

void operator delete( void* raw_memory ) thrwo()

{

if( raw_memory == 0 ){

return;

}

...//now,free raw memory block.

}

而对于member版本的也很简单.

void Base::operator delete( void* raw_memory,std::size_t size ) throw()

{

if( raw_memory == 0 ){

return;

}

if( size != sizeof(Base) ){ //if size error, call standard operator delete

::operator delete( raw_memory );

return;

}

...//now,free your raw memory block.

return;

}

如果即将删除的对象派生自某个base class而后者欠缺virtaul析构函数,那么C++传给operator delete的size_t数值可能不正确.这是'让你的base

class拥有virtual析构函数'的一个够好的理由.

请记住:

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler.

它也应该有能力处理0bytes申请.class专属版本则还应该处理'比正确大小更大的(错误)申请'.

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

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

我们都知道当你在写一个new表达式像这样:

Widget* new_widget = new Widget;

共有两个函数被调用:一个是用以分配内存的operator new,一个是Widget的default构造函数.

那么假设我们现在遇到的情况是:第一个函数调用成功,第二个函数却抛出异常.按照常理,在步骤一中所分配的

内存必须取消,否则就会造成内存泄露.而在这个时候客户已经没有能力归还内存了,因为手上没有指向这块内存的

指针,故此任务就落到了C++运行期系统身上.

为了完成任务,运行期系统当然就会调用步骤一所用的operator new的相应operator delete版本.如果当前要处

理的是拥有正常签名的new和delete版本,这好办!因为正常的operator new签名式:

void* operator new(std::size_t) throw (std::bad_alloc);

对应的正常的operator delete签名式:

void operator delete(void* raw_memory)throw();//global 作用域中的正常签名式

void operator delete(void* raw_memory,std::size_t size)throw();//class作用域中典型的签名式

因此,当你只使用正常形式的new和delete,运行期系统毫无问题可以找出那个'知道如何取消new所作所为并恢

复旧观'的delete.然而当你开始声明非正常形式的operator new,即就是附加参数的operator new,问题就出来了.

为了说明这个问题,我们依然用Widget例子,假设你写了一个class专属的operator new,要求接受一个ostream,

用来logged相关分配信息,同时又写了一个正常形式的class专属operator delete:

struct Widget{

//非正常形式的new

static void* operator new(std::size_t size,std::ostream& log_stream)throw(std::bad_alloc);

//正常class专属delete.

static void operator delete(void* memory, std::size_t size)throw();

...

};

在这里我们定义:如果operator new接受的参数除了一定会有的那个size_t之外还有其它,这个便是所谓的

placement new.而众多的placement new版本中特别提到的是'接受一个指针指向对象该被构造之处',那样的

operator new长相如下:

void* operator new( std::size_t, void* memory ) throw();//placement new

该版本的new已经被纳入C++标准程序库,你只要#include <new>就可以取用它,它的用处就是负责在vector的未

使用空间上创建对象.

现在让我们回到Widget的声明式,这个Widget将引起微妙的内存泄漏.考虑下面的测试代码,它将在动态创建一个

Widget时将相关的分配信息志记与cerr:

Widget* new_widget = new( std::cerr ) Widget;

在说一下我们先前提到的问题,如果内存分配成功,而构造抛出异常,运行期就有责任取消operator new的分配并

恢复旧观.然而运行期系统无法知道真正被调用的那个operator new如何运作,因此它无法取消分配并恢复旧观,所以

上述做法行不通.取而代之的是,运行期系统寻找'参数个数和类型都与operator new相同'的某个operator

delete.如果找打,那就是它该调用的对象.既然这里的operator new接受的类型为ostream&的额外实参,所以对应的

operator delete就应该是:

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

类似于new的placement版本,operator delete如果接受额外参数,便称为placement deletes.现在,既然Widget

没有声明placement版本的operator delete,所以运行期系统不知道如何取消并恢复原先对placement new的调用.于

是什么也不做.本例之中如果构造抛出异常,不会有任何operator delete被调用.

为了消除Widget中的内存泄漏,我们来声明一个palcement delete,对应与那个有志记功能的placement new:

struct Widget{

static void* operator new(std::size_t size, std::ostream& log_stream)throw(std::bad_alloc);

static void operator delete(void* memory) throw();

static void operator delete(void* memory,std::ostream& log_stream)throw();

...

};

这样改变之后,如果以下语句引发Widget构造函数抛出异常:

Widget* new_widget = new (std::cerr) Widget; //一如既往,但这次就不在发生泄漏.

然而如果没有抛出异常(大部分是这样的),客户代码中有个对应的delete,会发生什么事情:

delete pw; //call normal operator delete

调用的是正常形式的operator delete,而非其placement版本.请记住:placement delete只有在'伴随placement

new调用而触发的构造函数'出现异常时才会被调用.对着一个指针施行delete绝不会导致调用placement delete.

还有一点你需要注意的是:由于成员函数的名称会遮盖其外围作用域中的相同名称,你必须小心避免让class专属

news遮盖客户期望的其它news(包括正常版本).默认情况下,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();//nothrow new.see Item 49.

如果你在class内声明任何operator news,它会遮掩上述这些标准形式.除非你的意思就是要阻止class的客户使

用这些形式,否则请确保它们在你所生成的任何定制型operator new之外还可用.对于每一个可用的operator new也

请确定提供对应的operator delete.如果希望这些函数都有着平常的行为,只要令你的class专属版本调用global版

本即可.

为了完成以上所言的一个简单做法就是建立一个base class,内含所有正常形式的new和delete:

struct StandardNewDeleteForms{

//normal new/delete

static void* operator new(std::size_t size)throw(std::bad_alloc)

{ return ::operator new( size ); }

static void operator delete(void* memory) throw()

{ ::operator delete( memory ); }

//placement new/delete

static void* operator new(std::size_t size,void* pointer)throw()

{ return ::operator new( size, pointer );}

static void operator delete(void* memory,void* pointer)throw()

{ return ::operator delete( memory, pointer ); }

//nothrow new/delete

static void* operator new(std::size_t size,const std::nothrow_t& no_throw)throw()

{ return ::operator new( size, no_throw ); }

static void operator delete(void* memory,const std::nothrow_t&)throw()

{ ::operator delete( memory ); }

};

凡是想自定形式扩充标准形式的客户,可利用继承机制及using声明式(Item 33)取得标准形式:

struct Widget:public StandardNewDeleteForms{

using StandardNewDeleteForms::operator new;

using StandardNewDeleteForms::operator delete;

static void* operator new(std::size_t size, std::ostream& log_stream) throw(std::bad_alloc);

static void operator delete(void* memory,std::ostream& log_stream) throw();

...

};

请记住:

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

.

当你声明placement new和placement delete,请确定不要无意识地遮掩它们的正常版本.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: