这一章讲述了C++中的new和delete背后的一些机制,以及重写new和delete需要遵守的一些规则,以及什么时候适合重写new和delete以提高效率。
Item 49: 了解new-handler的行为
当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只能保证在分配内存时不抛出异常。
Item 50: 了解new和delete的合理替换时机
为什么会想要替换编译器提供的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)为了获得非传统的行为。
Item 51: 编写new和delete时需固守常规
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;
}
Item 52: 写了placement new也要与placement delete
所谓的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的一些基本东西。到此,这本书就总结完了。然后寒假也结束了~~~希望接下来的一学年好运吧。
分享到:
相关推荐
11. **内存管理**:讲解了内存分配和释放的策略,如使用`new`和`delete`,以及如何避免内存碎片。 12. **标准库工具**:介绍了一些不那么为人所知的库组件,如`std::swap`、`std::bitset`和`std::function`,以及...
《More Effective C++》这本书不仅深入浅出地讲解了许多 C++ 中的关键概念和技术,还提供了大量的实践经验和技巧。通过阅读这本书,读者不仅可以获得理论知识,还能学到如何在实际编程中应用这些知识,从而写出更...
4. **动态内存管理**:讲解了new和delete的使用细节,包括使用数组形式的new和delete,以及如何避免内存泄漏。还提到了RAII(Resource Acquisition Is Initialization)原则,它是智能指针和其他自动资源管理技术的...
内容简介: 有人说C++程序员可以分成两类,读过Effective C++的和没读过的。世界顶级C++大师Scott Meyers成名之作的第三版的确当得起这样的评价...8.定制new和delete 9.杂项讨论 A 本书之外 B 新旧版条款对映 索引
- **轻薄短小**:与传统的C++巨著不同,《More Effective C++》以其精炼的内容和紧凑的篇幅脱颖而出。 - **高密度经验分享**:作者通过多年在C++领域的实践经验,提炼出了许多实用的编程技巧和最佳实践。 - **深度...
不过,考虑到您提到的文件名是“Effective C++ 第三版.pdf”,我可以根据这本书的主题和内容来生成与C++编程语言相关的知识点。 《Effective C++》是由Scott Meyers所著的一本经典书籍,它深入讲解了C++编程中的...
- **使用new和delete代替malloc和free**:C++的`new`和`delete`操作符不仅分配和释放内存,还调用构造函数和析构函数,这使得资源管理更加自动化和安全。 - **一致使用new和delete**:在类中,如果成员变量通过`new`...
比如,条款1介绍了“使用new和delete时要考虑配对”,阐述了在使用动态内存时,必须确保正确地匹配使用new和delete,防止内存泄漏。条款28则讲解了“考虑用虚析构函数替代纯虚析构函数”,在设计基类时,是否需要...
- ITEM M8讲解了`new`和`delete`操作符的不同用法,以及何时使用配对的`new[]`和`delete[]`来处理动态数组。 4. 异常部分(未在提供的内容中详细展开)可能涉及C++的异常处理机制,包括何时和如何抛出、捕获异常,...
《More Effective C++》是C++编程领域的一本经典之作,由Scott Meyers撰写,中文版的译序和导读由知名C++专家侯捷完成。这本书深入探讨了C++编程中的高级技巧和最佳实践,旨在帮助程序员提升代码质量和效率。 在...
4. **使用动态内存**:了解new和delete的用法,避免内存泄漏,掌握智能指针的使用。 5. **理解引用**:了解引用的特点,何时使用引用而非指针,以及引用的生命周期。 6. **利用运算符重载**:理解何时及如何重载...
正确使用`new`和`delete`,以及智能指针(如`std::unique_ptr`和`std::shared_ptr`),可以防止内存泄漏和悬挂指针。 8. **使用RAII(Resource Acquisition Is Initialization)原则**:通过将资源的生命周期与对象...
1. **内存管理**:书中强调了对动态内存的理解,如智能指针(shared_ptr、unique_ptr、weak_ptr)的使用,以及何时应该使用new和delete,以及如何正确处理资源的生命周期。 2. **构造与析构**:讨论了构造函数和...
根据提供的信息,我们可以总结出《Effective C++, 3rd Edition》这本书主要涵盖了C++编程语言中的关键实践和技术建议。本书通过一系列具体的项目和实例来帮助读者改进程序设计和编码技巧。接下来,我们将深入探讨该...