8.定制new和delete Customizing new and delete

当计算环境(例如Java和.NET)夸耀自己内置“垃圾回收能力”的当今,C++对内存管理的纯手工法也许看起来有点老气。但是许多苛刻的系统程序开发人员之所以选择C++,就是因为它允许他们手工管理内存。这样的开发人员研究并学习他们的软件使用内存的行为特征,然后修改分配和归还工作,以求获得其所建置的系统的最佳效率(包括时间和空间)。

这样做的前提是,了解 C++内存管理例程的行为。这正是本章焦点。这场游戏的两个主角是分配例程和归还例程(allocation and deallocation routines,也就是operator new和operator delete),配角是new-handler,这是当operator new无法满足客户的内存需求时所调用的函数。

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

另外要记住的是,operator new和 operator delete只适合用来分配单一对象。A r r a y s 所用的内存由 operator new[]分配出来,并由 operator delete[]归还(注意两个函数名称中的[])。除非特别表示,我所写的每一件关于operator new和operator delete的事也都适用于operator new[]和operator delete[]。

最后请注意,STL 容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,不是被new和delete直接管理。本章并不讨论STL分配器。

条款49:了解new-handler的行为 Understand the behavior of the new-handler.

当 operator new无法满足某一内存分配需求时,它会抛出异常。以前它会返回一个null指针,某些旧式编译器目前也还那么做。你还是可以取得旧行为(有那么几分像啦),但本条款最后才会进行这项讨论。

当 operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。(这其实并非全部事实。operator new真正做的事情稍微更复杂些。详见条款51。)为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler,那是声明于<new>的一个标准程序库函数:

namespace std {
	typeed void (*new_handler) ();
	new_handler set_new_handler(new_handler p) throw();
}

如你所见,new_handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西。set_new_handler则是“获得一个new_handler并返回一个new_handler”的函数。set_new_handler声明式尾端的 "throw()" 是一份异常明细,表示该函数不抛出任何异常——虽然事实更有趣些,详见条款29。

set_new_handler的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要被替换)的那个new-handler函数。

你可以这样使用set_new_handler:

//以下是当operator new无法分配足够内存时,该被调用的函数
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会被调用,于是程序在发出一个信息之后夭折(abort)。(顺带一提,如果在写出错误信息至cerr过程期间必须动态分配内存,考虑会发生什么事……)

当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。引起反复调用的代码显示于条款51,这里的高级描述已足够获得一个结论,那就是一个设计良好的new-handler函数必须做以下事情:

■ 让更多内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的一个做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。

■ 安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个 new-handler 有此能力。果真如此,目前这个 new-handler就可以安装另外那个new-handler以替换自己(只要调用set_new_handler)。下次当 operator new调用 new-handler,调用的将是最新安装的那个。(这个旋律的变奏之一是让new-handler修改自己的行为,于是当它下次被调用,就会做某些不同的事。为达此目的,做法之一是令 new-handler 修改“会影响new-handler行为”的static数据、namespace数据或global数据。)

■ 卸除new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。

■ 抛出bad_alloc (或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。

■ 不返回,通常调用abort或exit。

这些选择让你在实现new-handler函数时拥有很大弹性。

有时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属于哪个class而定:

class X {
public:
	static void outOfMemory();
	...
}
class Y {
public:
	static void outOfMemory();
	...
}
X *p1 = new X;//如果分配不成功,调用X::outOfMemory
Y *p2 = new Y;//如果分配不成功,调用Y::outOfMemory

C++并不支持 class 专属之 new-handlers,但其实也不需要。你可以自己实现出这种行为。只需令每一个 class 提供自己的 set_new_handler和 operator new即可。其中set_new_handler使客户得以指定class专属的new-handler(就像标准的set_new_handler允许客户指定global new-handler),至于operator new则确保在分配 class 对象内存的过程中以 class 专属之 new-handler 替换 global new-handler。

现在,假设你打算处理Widget class的内存分配失败情况。首先你必须登录“当operator new无法为一个Widget对象分配足够内存时”调用的函数,所以你需要声明一个类型为new_handler的static成员,用以指向class Widget的new-handler。看起来像这样:

class Widget {
public: 
	static std::new_handle 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定义式之外被定义(除非它们是const而且是整数型,见条款2),所以需要这么写:

std::new_handler Widget::currentHandler = 0;//在class实现文件内初始化为null

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做以下事情:

1. 调用标准set_new_handler,告知Widget的错误处理函数。这会将Widget的new-handler安装为global new-handler。

2. 调用 global operator new,执行实际之内存分配。如果分配失败,global operator new会调用Widge的new-handler,因为那个函数才刚被安装为global new-handler。如果 global operator new 最终无法分配足够内存,会抛出一个bad_alloc异常。在此情况下 Widget的 operator new必须恢复原本的 global new-handler,然后再传播该异常。为确保原本的new-handler总是能够被重新安装回去,Widget将global new-handler视为资源并遵守条款13的忠告,运用资源管理对象(resource-managing objects)防止资源泄漏。

3. 如果global operator new能够分配足够一个Widget对象所用的内存,Widget的 operator new 会返回一个指针,指向分配所得。Widget 析构函数会管理global new-handler,它会自动将Widget’s operator new被调用前的那个global new-handler恢复回来。

下面以C++代码再阐述一次。我将从资源处理类(resource-handling class)开始,那里面只有基础性 RAII 操作,在构造过程中获得一笔资源,并在析构过程中释还(见条款13)

class NewHandlerHolder {
public:
	explicit NewHandlerHolder(std::new_handler nh) //取得目前的new-hander.
	:handler(nh) { }
	~NewHandlerHolder()
	{ std::set_new_handler(handler);}				//释放它
private:
	std::new_handler handler;						//记录下来。
	NewHandlerHolder(const NewHandlerHolder&);		//阻止copying
	NewHandlerHolder &operator=(const NewHandlerHolder&);//(见条款14)
};

这就使得Widget’s operator new的实现相当简单:

void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
	NewHandlerHolder h(std::set_new_handler(currentHandler));	//安装Widget的new-handler
	ruturn ::operator new(size);								//分配内存或抛出异常
}																//恢复global new-handler

Widget的客户应该类似这样使用其new-handling:

void outOfMem();					//函数声明,此函数在Widget对象分配失败时被调用。
Widget::set_new_handler(outOfMem);	//设定outOfMem为Widget的new-handling函数
Widget *pw1 = new Widget;			//如果内存分配失败,调用outOfMem.
std::string *ps = new std::string;	//如果内存分配失败,调用global new-handling函数(如果有的话)
Widget::set_new_handler(0);			//设定Widget专属的new-handling函数为null
Widget* pw2 = new Widget;			//如果内存分配 失败,立刻抛出异常(class Widget并没有专属的new-handling函数)

实现这一方案的代码并不因 class 的不同而不同,因此在它处加以复用是个合理的构想。一个简单的做法是建立起一个 "mixin" 风格的base class,这种base class用来允许 derived classes 继承单一特定能力——在本例中是“设定 class 专属之new-handler”的能力。然后将这个base class转换为template,如此一来每个derived class将获得实体互异的class data复件。

这个设计的base class部分让derived classes继承它们所需的set_new_handler和operator new,而template部分则确保每一个derived class获得一个实体互异的currentHandler 成员变量。听起来似乎有点复杂,但代码非常近似前个版本。实际上,唯一真正意义上的不同是,它现在可被任何有所需要的class使用:

template<typename T>	//"mixim"风格的base class,用以支持class专属的set_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);
	...					//其他的operator new 版本————见条款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)
{
	NewHandlerSupport h(std::set_new_handler(currentHandler));
	return ::operator new(size);
}
//以下将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

有了这个class template,为Widget添加set_new_handler支持能力就轻而易举了:只要令Widget继承自NewHandlerSupport&lt;Widget&gt;就好,像下面这样。看起来似乎很奇妙,稍后我将更详细解释它的精确意义。

class Widget: public NewHandlerSupport<Widget> {
	...				//和先前一样,但不必声明
};					//set_new_handler或operator new

这就是Widget为了提供“class专属之set_new_handler”所需做的全部动作。

但或许你还是对Widget继承NewHandlerSupport&lt;Widget&gt;感到心慌意乱。果真如此,你的焦虑还可能因为注意到NewHandlerSupport template从未使用其类型参数 T 而更放大数倍。实际上 T 的确不需被使用。我们只是希望,继承自NewHandlerSupport的每一个class,拥有实体互异的NewHandlerSupport复件(更明确地说是其static成员变量currentHandler)。类型参数T只是用来区分不同的derived class。Template机制会自动为每一个T(NewHandlerSupport赖以具现化的根据)生成一份currentHandler。

至于说到 Widget继承自一个模板化的(templatized)base class,而后者又以Widget作为类型参数,如果你对此头昏眼花,不要觉得惭愧。每个人一开始都有那种反应。由于它被证明是一个有用的技术,因此甚至拥有自己的名称:“怪异的循环模板模式”(curiously recurring template pattern;CRTP)。有些人认为这个名称给人的第一眼印象很不自然。嗯,确实如此。

我曾发表过一篇文章,建议给它一个比较好的名称,像是Do It For Me,因为当Widget继承 NewHandlerSupport&lt;Widget&gt;时它其实并不是说“我是Widget,我要针对Widget class继承NewHandlerSupport”。但是,哎,没人采用我建议的名称(甚至我自己也不),但如果你看到CRTP会联想到它说的是"do it for me",或许可以帮助你了解这一模板化继承(templatized inheritance)到底用意为何。

像NewHandlerSupport这样的templates,使得“为任何class添加一个它们专属的new-handler”成为易事。然而 "mixin" 风格的继承肯定导致多重继承的争议,而在开始那条路之前,你需要先阅读条款40。

直至1993年,C++都还要求 operator new必须在无法分配足够内存时返回null。新一代的operator new则应该抛出bad_alloc异常,但很多C++程序是在编译器开始支持新修规范前写出来的。C++标准委员会不想抛弃那些“侦测null”的族群,于是提供另一形式的 operator new,负责供应传统的“分配失败便返回null”行为。这个形式被称为 "nothrow" 形式——某种程度上是因为他们在new的使用场合用了nothrow对象(定义于头文件&lt;new&gt;):

class Widget {...};
Widget *pw1 = new Widget;					//如果分配失败,抛出bad_alloc
if(pw1 == 0) ...							//这个测试一定失败
Widget *pw2 = new (std::nothrow) Widget;	//如果分配Widget失败,返回0
if(pw2 == 0) ... 							//这个测试可能成功

Nothrow new对异常的强制保证性并不高。要知道,表达式 "new (std::nothrow) Widget" 发生两件事,第一,nothrow 版的 operator new被调用,用以分配足够内存给Widget对象。如果分配失败便返回null指针,一如文档所言。如果分配成功,接下来 Widget构造函数会被调用,而在那一点上所有的筹码便都耗尽,因为Widget构造函数可以做它想做的任何事。它有可能又 new一些内存,而没人可以强迫它再次使用 nothrow new。因此虽然 "new (std::nothrow) Widget" 调用的operator new并不抛掷异常,但Widget构造函数却可能会。如果它真那么做,该异常会一如往常地传播。需要结论吗?结论就是:使用 nothrow new 只能保证operator new不抛掷异常,不保证像 "new (std::nothrow) Widget"这样的表达式绝不导致异常。因此你其实没有运用nothrow new的需要。

无论使用正常(会抛出异常)的new,或是其多少有点发育不良的nothrow兄弟,重要的是你需要了解new-handler的行为,因为两种形式都使用它。

请记住

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值