effective c++ -- 定制new和delete

本文探讨了C++中new和delete操作符的内部机制,包括如何重写这些操作符以优化内存管理和错误处理。讨论了new-handler的作用及如何通过set_new_handler进行设置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这一章讲述了C++中的new和delete背后的一些机制,以及重写new和delete需要遵守的一些规则,以及什么时候适合重写new和delete以提高效率。

[b]Item 49: 了解new-handler的行为[/b]
当operator new无法满足某一内存分配需求时,它会调用一个客户指定的错误处理函数,及new-handler,new-handler可能会为operator new找到足够的内存或者其他怎样以处理内存不足的情况,如果new-handler为空,operator new抛出异常。
通过set_new_handler来设置内存不足时调用的操作,它是声明于<new>的一个标准程序库函数:

namespace std{
typedef void (*new_handler) ();
new_handler set_new_handler( new_handler p ) throw(); //承诺不抛出异常
};
//这样使用set_new_handler
void outOfMem(){
std::err<<"Unable to satisfy request for memory\n";
std::abort();
}

int main(){
std::set_new_handler( outOfMem );
int* pBigDataArray = new int[10000000L];
...
}

当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存,或者最后抛出异常。因此,一个设计良好的new-handler函数必须做以下事情:
(1)让更多内存可被使用。实现此策略的做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序。
(2)安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力,因为可以通过set_new_handler安装一个新的new-handler。
(3)卸载new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。
(4)抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
(5)不返回,通常调用abort或exit。
假如你希望以不同方式处理内存分配失败情况,具体视class而定,也就是提供class之专属new-handler,你可以自己实现出这种行为,只需要令每一个class提供自己的set_new_handler和operator new即可,其中set_new_handler使客户得以指定class专属的new-handler,而operator new则确保在分配class对象内存的过程中以class专属之new-handler替换global new-handler。

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 = 0;
std::new_handler Widget::set_new_handler( std::new_handler p )throw(){
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
};

Widget专属的operator new做的事情就是在调用global operator new之前将global new handler安装成Widget专属的new-handler,并在成功分配内存或抛出异常前将原先的global new-handler安装回去。为了达到这一点,可以使用资源管理类:

class NewHandlerHolder{
public:
explicit NewHandlerHolder( std::new_handler nh ):handler(nh){}
~NewHandlerHolder(){
std::set_new_handler(handler); }
private:
std::new_handler handler;
//阻止copying
NewHandlerHolder( const NewHandlerHolder& );
NewHandlerHolder& operator=( const NewHandlerHolder& );
};
//于是Widget::operator new的实现就相当简单了:
void* Widget::operator new( std::size_t size ) throw ( std::bad_alloc ){
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}

事实上,所有希望实现专属set_new_handler的类在这方面的实现基本没有差异,因此我们其实可以把它总结成一个基类:

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*( std::size_t size ) throw( std::bad_alloc ){
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}

这里的template T完全没有被使用,但是,它确保了每一个derived class获得一个实体互异的currentHandler。

最后,可以使用

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

来获得返回0而不是抛出异常的operator new,但是它只保证operator new不抛出异常,并不保证后续的构造函数不抛出异常,要知道,operator new分配完内存后,还会使用构造函数初始化这片内存。而nothrow只能保证在分配内存时不抛出异常。

[b]Item 50: 了解new和delete的合理替换时机[/b]
为什么会想要替换编译器提供的operator new或operator delete呢?下面是三个常见的理由:
(1)用来检测运用上的错误。如果将“new所得内存delete”却不幸失败,会导致内存泄漏。如果在“new所得内存”身上多次delete则会导致不确定行为。如果让operator new持有一串动态分配所得的地址,而operator delete将地址从中移动,便可以很容易地检测出上述错误用法。另外可以在operator new时超额分配内存,以额外空间放置特定的byte pattern,operator delete便得以检查上述签名是否原封不动,若否的话说明在分配区的某个生命时间发生了overrun或underrun。可以通过operator delete将出错的指针载入日志。
(2)为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它处理的内存请求有时很大,有时很小,它必须处理大数量短命对象的持续分配和归还。它们必须考虑碎片问题。定制版的operator new和operator delete通常在性能上用过缺省版本,它们运行得比较快,需要的内存比较少。
(3)为了收集使用上的统计数据。在定制特定new和delete之前,我们应该收集软件如何使用其动态内存:分配区块的大小分布如何?寿命分布如何?它们倾向于使用FIFO还是LIFO还是随机次序来分配和归还?它们的运用形态是否随时间改变?等等。
(4)为了弥补缺省分配器中的非最佳齐位。
(5)为了将相关对象成簇集中。
(6)为了获得非传统的行为。

[b]Item 51: 编写new和delete时需固守常规[/b]
operator new必须遵守的规矩:返回正确的值,内存不足时必得调用new-handling函数,必须有对付零内存的准备,还需避免不慎掩盖正常形式的new。C++规定,即使客户要求0 bytes,operator new也得返回一个合法指针。下面是个non-member operator new的伪码:

void* operator new( std::size_t, size ) throw (std::bad_alloc ){
using namespace std;
if( size == 0 ) //处理0 byte申请
size = 1;
while( true ){
//尝试分配size bytes
void* pMem = malloc(size);
if( pMem ) //成功分配
return pMem;
//分配失败
//获取当前的new_handler
new_handler globalHandler = set_new_handler(0);
set_new_handler( globalHandler );

if( globalHandler ) (*globalHandler)();
else throw std::bad_alloc();
}
}

对于class专属的operator new,还必须注意到它很可能被其派生类继承。而写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何派生类。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)对象而设计。然而一旦被继承下去,有可能基类的operator new被调用用以分配派生类对象:

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

Derieved* p = new Derieved; //这里调用的是Base::operator new
//因此,假如operator new是被专门设计用以返回Base对象大小的内存
//有必要为此情况做出一点处理:
void* Base::operator new( std::size_t size ) throw ( std::bad_alloc ){
if( size != sizeof(Base) )
//注意,sizeof(Base)一定不为0,所以size为0的情况将交给::operator new
return ::operator new(size);
...
}

如果你打算控制class专属之arrays内存分配行为,那么便需要实现operator new的array兄弟版:operator new[],这时你唯一需要做的一件事就是分配一块未加工内存。
至于operator delete,唯一需要记住的是:C++保证“删除null指针永远安全”。另外,如果你的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 == 0 )
return;
if( size != sizeof(Base){
::operator delete(rawMemory);
return;
}
return;
}


[b]Item 52: 写了placement new也要与placement delete[/b]
所谓的placement new,是相对于正常的operator new而言的,或者从广义上说,应该是“被重载的operator new”(我个人是这么感觉的)。
正常的operator new及其相应的operator delete版本如下:

void* operator new( std::size_t ) throw ( std::bad_alloc );
void operator delete( void* rawMemory ) throw ();
//class作用域中的delete,包含一个大小
void operator delete( void* rawMemory, std::size_t size )throw();

new表达式:

Widget* pw = new Widget;

实际调用了两个函数,一个是用以分配内存的operator new,一个是Widget的default构造函数。
假设其中第一个函数调用成功,即内存已经被分配,第二个函数却抛出异常。此时,pw尚未被赋值,被内存已经被分配,但是我们没有得到指向该内存的指针,因此我们没有能力释放被分配的内存。因此,释放内存的责任就落在了C++运行期系统上。运行期系统将会找到与刚才调用的operator new相配对的operator delete来执行这一任务。
假设我们为Widget写了一个专属的operator new,要求接受一个ostream用以记录分配信息:

class Widget{
public:
static void* operator new( std::size_t size, std::ostream& log )
throw( std::bad_alloc );
...
};

“如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new”。
最早的placement new版本是一个“接受一个指针指向对象被构造之处”的operator new,其长相如下:

void* operator new( std::size_t, void* pMemory ) throw();

它的用途之一是负责在vector的未使用空间上创建对象。
如果在调用

Widget* pw = new (std::err) Widget;

时Widget构造函数抛出异常,C++运行期系统将自动调用与operator new相对应的版本来释放内存。所谓的“相对应的版本”,是指“参数个数和类型都与operator new相同”的某个operator delete,因此,当你定义了一个placement new时,你必须定义一个对应的placement delete,否则系统找不到对应的delete,无法执行内存回收工作,那么内存就泄漏了。
另外,operator new函数的名字匹配规则同一般的成员函数一样,如果你只在基类里声明了一个placement new,关于基类的new调用将无法使用正常版本;然后如果你又在继承自该基类的派生类里重新声明正常形式的new,基类的placement new也将不能默认作用在该派生类上。总之就是,operator new的名字匹配规则跟一般的成员函数是一样的。
placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。

书中最后一章提醒了编译器warning的重要性,介绍了std::tr1及boost的一些基本东西。到此,这本书就总结完了。然后寒假也结束了~~~希望接下来的一学年好运吧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值