您的位置:首页 > 其它

第八章 定制new和delete

2014-08-11 21:28 225 查看
本章意在了解C++内存管理例程的行为。这场游戏的两个主角是分配例程和归还例程(也就是operator new和operator delete),配角是new-handler,这个是operator new无法满足客户的内存 需求时所调用的函数。

多线程环境下的内存管理,遭受单线程系统不曾有过的挑战。由于heap是一个可被改动的全局性资源,因此多线程系统充斥发狂访问这一类资源的race conditions出现机会。本章多个条款提及使用可改动之static数据,这总是会令线程感知程序高度警戒如坐针毡。如果没有适当的同步控制,一旦使用无锁算法或精心防止并发访问时,调用内存例程可能很容易导致管理heap的数据结构内容败坏。我不想一再提醒你这些危险,我只打算在这里提一下,然后假设你会牢记在心。

另外要记住的是,operator new和operator delete只适合用来分配单一对象。Arrays所用的内存由operator new[] 分配出来,并由operator delete[]归还。

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

在我们平时的编写代码过程中,总是避免不了去new出一些对象出来.我们知道new操作符私底下通过调用operator new来实现内存分配的.当operator new抛出异常以反映一个未获满足的内存需求之前,它会调用一个客户指定的错误处理函数,一个所谓的new-handler.而客户是通过set_new_handler将自己的new-handler传递给它的,其声明在<new>标准库函数中:

C++ Code
1

2

3

4

5

namespace std

{

typedef void (*new_handler)();

new_handler set_new_handler( new_handler p ) throw();

}
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存.关于反复调用代码我们在条款51中我们会讨论.一个设计良好的new-handler函数必须做以下事情:

■ 让更多内存可被使用.

■ 安装另一个new-handler.

■ 卸除new-handler

■ 抛出bad_alloc(或派生自bad_alloc)的异常.

■ 不返回.

这些选择让你在实现new-handler函数时拥有很大的弹性.而有时候你会希望'依据不同的类,调用附属与该类的new-handler,看起来也许是这个样子:

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

struct TestA

{

static void outOfMemory();

...

};

struct TestB

{

static void outOfMemory();

...

};

TestA *a = new TestA;//if memory runs out,call TestA::outOfMemory

TestB *b = new TestB;//if memory runs out,call TestB::outOfMemory
但是C++不支持class专属之new-handlers,但你可以自己实现这种行为.好,我们来试试!

假设我们现在打算处理Widget class的内存分配失败情况,我们需要声明一个类型为new_handler的static

成员,用以指向class Widget的new-handler,看起来应该像这样:

C++ Code
1

2

3

4

5

6

7

struct Widget

{

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 st_current_handler_;

};
Widget内的set_new_handler函数会将它获得的指针存储起来,然后返回调用之前的指针,这跟标准版本的set_new_hander的行为是一样的:

C++ Code
1

2

3

4

5

6

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

{

std::new_handler old_handler = st_current_handler_;

st_current_handler_ = p;

return old_handler;

}
下面我将构造一个资源处理类来操作new-handler,在构造过程中获得资源,在析构过程中释放:

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

13

struct NewHandlerHolder

{

explicit NewHandlerHolder(std::new_handler handler)

: handler_( handler ) {}

~NewHandlerHolder()

{

std::set_new_handler( handler_ );

}

private:

NewHandlerHolder(const NewHandlerHolder &);

NewHandlerHolder &operator=(const NewHandlerHolder &);

std::new_handler handler_;

};
这样Widget内的operator new的实现就变的容易了:

C++ Code
1

2

3

4

5

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

{

NewHandlerHolder alloc_handler( std::set_new_handler( st_current_handler_ ) );

return ::operator new( size );

}
客户现在可以这样用:

C++ Code
1

2

3

4

5

6

7

8

9

10

void outOfMemory();

Widget::set_new_handler( outOfMemory );

Widget *new_widget_obj = new Widget; //if memory allocation failed,

//call outOfMemory

std::string *new_string = new std::string; //if memory allocation failed,

//call global new-handling function

//if exists.

Widget::set_new_handler( 0 );

Widget *another_widget = new Widget; // if memory allocation failed,throw

//exception at once.
其实我们可以用base class将new-handler操作独立出来,让Widget继承自base class,呵呵,这样是不是更好:

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

template<typename T>

struct NewHandlerSupport

{

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

{

std::new_handler old_handler = st_current_handler_;

st_current_handler_ = p;

return old_handler;

}

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

{

NewHandlerHolder h( std::set_new_handler( st_current_handler_ ) );

return ::operator new( size );

}

...

private:

static std::new_handler st_current_handler_;

};

template<typename T>

//std::new_handler NewHandlerSupport<T>::st_current_handler_ = 0;

//有了这个template, 为Widget添加set_new_handler支持能力就轻而易举了:

struct Widget: public NewHandlerSupport<Widget>

{

...

};
好了,这个问题的讨论,就结束了.我们来把目光转到Nothrow new.

nothrow版的operator new被调用,用以分配足够内存给Widget对象.如果分配失败便返回null指针,一如文档所言.所有分配成功,接下来Widget构造函数会被调用,而在那一点上所有的筹码便被耗尽,因为Widget构造函数可以做它想做的任何事情.它有可能又new一些内存,而没人可以强迫它再次使用nothrownew.现在我们可以得出结论:使用nothrow new只能保证operaor new不抛掷异常,不保证像'new (std::nothrow) Widget'这样的表达式不导致异常,其实你没有运用nothrow
new的需要.

请记住:

■ 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了,我的初稿看起来如下述代码所示,其中还存

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

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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伪码:

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

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对象:

C++ Code
1

2

3

4

5

6

7

8

9

10

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,像这样:

C++ Code
1

2

3

4

5

6

7

8

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的伪码:

C++ Code
1

2

3

4

5

6

7

8

void operator delete( void *raw_memory ) thrwo()

{

if( raw_memory == 0 )

{

return;

}

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

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

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

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表达式像这样:

C++ Code
1

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

那么假设我们现在遇到的情况是:第一个函数调用成功,第二个函数却抛出异常.按照常理,在步骤一中所分配的内存必须取消,否则就会造成内存泄露.而在这个时候客户已经没有能力归还内存了,因为手上没有指向这块内存的指针,故此任务就落到了C++运行期系统身上.

为了完成任务,运行期系统当然就会调用步骤一所用的operator new的相应operator delete版本.如果当前要处理的是拥有正常签名的new和delete版本,这好办!因为正常的operator new签名式:

C++ Code
1

void *operator new(std::size_t) throw (std::bad_alloc);
对应的正常的operator delete签名式:

C++ Code
1

2

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:

C++ Code
1

2

3

4

5

6

7

8

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长相如下:

C++ Code
1

void *operator new( std::size_t, void *memory ) throw();//placement new
该版本的new已经被纳入C++标准程序库,你只要#include <new>就可以取用它,它的用处就是负责在vector的未

使用空间上创建对象.

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

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

C++ Code
1

Widget *new_widget = new( std::cerr ) Widget;
在说一下我们先前提到的问题,如果内存分配成功,而构造抛出异常,运行期就有责任取消operator new的分配并恢复旧观.然而运行期系统无法知道真正被调用的那个operator new如何运作,因此它无法取消分配并恢复旧观,所以上述做法行不通.取而代之的是,运行期系统寻找'参数个数和类型都与operator new相同'的某个operator delete.如果找打,那就是它该调用的对象.既然这里的operator new接受的类型为ostream&的额外实参,所以对应的operator
delete就应该是:

C++ Code
1

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:

C++ Code
1

2

3

4

5

6

7

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构造函数抛出异常:

C++ Code
1

Widget *new_widget = new (std::cerr) Widget; //一如既往,但这次就不在发生泄漏.
然而如果没有抛出异常(大部分是这样的),客户代码中有个对应的delete,会发生什么事情:

C++ Code
1

delete pw; //call normal operator delete
调用的是正常形式的operator delete,而非其placement版本.请记住:placement delete只有在'伴随placement new调用而触发的构造函数'出现异常时才会被调用.对着一个指针施行delete绝不会导致调用placement delete.

还有一点你需要注意的是:由于成员函数的名称会遮盖其外围作用域中的相同名称,你必须小心避免让class专属news遮盖客户期望的其它news(包括正常版本).默认情况下,C++在global作用域内提供以下形式的operator new:

C++ Code
1

2

3

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:

C++ Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

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)取得标准形式:

C++ Code
1

2

3

4

5

6

7

8

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,请确定不要无意识地遮掩它们的正常版本.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: