条款51:编写new和delete时需固守常规
条款50已经解释什么时候你会想要写个自己定制的operator new和operator delete,这个条款告诉你编写定制的内存管理器时必须遵守什么规则。
operator new应该内含一个无穷循环,并在其中尝试分配内存。实现一致性operator new必须返回正确地值,内存不足时必须调用new-handler函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new。因为operator new实际上不止一次尝试分配内存,并在每次失败后调用new-handler函数。只有当指向new-handler函数的指针是null,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)
{
尝试分配size bytes;
if(分配成功)
return(一个指针,指向分配得来的内存);
//分配失败,找出目前的new-handler函数
new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if(size!=sizeof(Base))
return::operator new(size);//令标准operator new处理
...
}
如果你决定写一个operator new[],记住,唯一需要做的事情就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。包括假设array内的每个对象的大小是sizeof(base),或元素个数是byte申请数/sizeof(base)等等。
operator delete的情况更简单,你需要记住的唯一事情就是C++保证“删除null指针永远安全”,所以你必须兑现这个承诺。
下面是non-member operator delete的伪码:
void operator delete(void* rawMemory) throw()
{
if(rawMemory==0) return;//如果被删除的是一个null指针,那就什么都不做
现在,归还rawMemory所指的内存;
}
这个函数的member版本也很简单,只需要多加一个动作检查删除数量。 万一你的class专属的operator new将大小有误的分配行为转交标准new执行,你也必须将大小有误的删除行为转交::operator delete执行:
void Base::operator delete(void* rawMemory,std::size_t size) throw()
{
if(rawMemory==0) return;
if(size!=sizeof(Base)){
::operator delete(rawMemory);//令标准版本处理此申请
return;
}
现在,归还rawMemory所指的内存;
return;
}
如果你的base class遗漏了virtual析构函数,operator delete可能无法正常工作,因为c++传给operator delete的size_t数值可能不正确。
条款52:写了placement new也要写placement delete
placement new名称的来源:operator new接受的参数除了一定会有的那个size_t之外该有其他,其中最特别的一个是“接受一个指针指向对象被构造之处”,那样的operator new长相如下:
void* operator new(std::size_t size,void* pMemory) throw();
这个版本的new已被纳入C++标准程序库,你只要#include<new>就可以使用它。实际上它正是placement new的命名根据:一个特定位置上的new。当人们说到属于placement new时,一般有两种含义:上述的operator new特定版本,另一个是带有任意额外参数的operator new。往往能够根据上下文推测出其含义。
引入:当你写y一个new表达式像这样:
Widget* pw=new Widget;//公有两个函数被调用,一个是用来分配内存的operator new,另一个是Widget的默认构造函数
//假设第一个函数调用成功,第二个函数却抛出异常,步骤一的内存分配所得必须取消并恢复旧观
//取消步骤一并恢复旧观的责任客户无法做到,这就要落在C++运行期系统身上
//运行期系统要调用步骤一中所调用的operator new相应operator delete版本
规则:当你只是用正常形式的operator new和delete时,运行期系统毫无疑问可以找到相对应的delete。但是当你使用一个非正常形式的new时,问题就出现了,系统不知道要调用哪一个delete版本。
当你写一个placement operator new,请确定写出相对应的placement operator delete,即两者带有相同额外参数。那么当new的内存分配动作需要取消并恢复旧观时就可以调用相对应的operator delete,消除代码中可能存在的内存泄露。
class Widget{
public:
...
static void* operator new(std::size_t,size,std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory)throw();//正常版本
static void operator delete(void* pMemory,std::ostream& logStream) throw();//placement delete
//static成员要在类外定义
...
};
//这样声明定义后,如果以下语句引发Widget构造函数抛出异常,则调用placement delete,不会引发内存泄露
Widget* pw=new Widget;
delete pw;//调用正常的operator delete
由于成员函数的名称会掩盖其外围作用域中的相同名称,你必须小心避免让class专属的new和delete掩盖客户期望的其他版本(包括正常版本)。同样的道理,derived class中的operator new和operator delete会掩盖global版本和继承而来的base class 版本。
对于撰写内存分配函数,你需要记住的是,缺省状态下C++在global作用域内提供一下形式的operator new:
void* operator new(std::size_t size) throw(std::bad_alloc);//normal new
void* operator new(std::size_t size,void*)throw();//placement new
void* operator new(std::size_t size,const std::nothrow_t&)throw();//nothrow new,内存分配失败时返回null,并不抛出异常
如果你在class内声明任何的operator new,它会遮掩上述这些标准形式。对每一个可用的operator new也请确定提供对应的operator delete。如果你希望这些函数有着正常的行为,只要令你的class专属版本调用global版本即可。
做法很简单,建立一个base class,内含所有正常行驶的new和delete:
class StandardNewDeleteForms{
public:
//normal new/delete
static void* operator new(std::size_t size)throw(std::bad_alloc)
{return ::operator new(size);}
static void operator delete(void* pMemory)throw()
{::operator delete(pMemory);}
//placement new/delete
static void* operator new(std::size_t size,void* ptr)throw()
{return::operator new(size,ptr);}
static void operator delete(void* pMemory,void* ptr)throw()
{return::operator delete(pMemory,ptr);}
//nothrow new/delete
static void* operator new(std::size_t,const std::nothrow_t& nt) throw()
{ return::operator new(size,nt);}
static void operator delete(void* pMemory,const std::nothrow_t& nt) throw()
{::operator delete(pMemory);}
凡是想以自定形式扩充标准形式的客户,可利用继承机制及using 声明式取得标准形式:
class Widget:public StandardNewDeleteForms{
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;//让这些形式可见
static void* operator new(std::size_t size,std::ostream& logstream)throw(std::bad_alloc)//自定义placement new
static void operator delete(void* pMemory,std::ostream& logstream)throw()//添加一个对应的placement delete
...
}
条款53:不要轻忽编译器的警告
问题:许多程序员习惯性忽略编译器的警告,他们认为如果问题很严重,编译器应该给出一个错误信息而不是警告信息。
引子:举个例子,下面是或多或少会发生在每个人身上的一个错误
class B{
public:
virtual void f() const;
};
class D:public B{
public:
virtual void f();
};
//这里希望以D::f重新定义virtual函数B::f,但是B中的f()是const成员,而在D中它没有被声明为const
于是有的编译器发出一个警告:f() hides virtual B::f()
这个编译器试图告诉你声明于B中的f()并没有在D中被重新声明,而是整个被遮掩了。如果忽略这个警告,几乎肯定导致错误的程序行为。
结论:所以,在你打发某个警告信息之前,请确定你了解它意图说出的精确意义。
严肃对待编译器发出的警告信息,努力在你的编译器的最高警告级别下争取“无任何警告”的荣誉。
不要过度依赖编译器的报警能力,因为不同的编译器对待事物的态度并不相同。一旦移植到另一个编译器上,你原本的警告信息可能会消失。
条款54:让自己熟悉包括TR1在内的标准程序库
TR1代表“Technical Report 1",是一份文档,TR1自身只是一份规范,它宣告了一个新版C++的来临,TR1提供的机能几乎对每一种程序库和每一种应用程序都带来利益。
在概括论述TR1有些什么之前,先回顾下C++标准程序库有哪些主要成分:
- STL(标准模板库),覆盖容器(如vector,string,map)、迭代器、算法(如find,sort,transform)、函数对象(如less,greater)、各种容器适配器和函数适配器
- Iostreams,覆盖用户自定缓冲功能、国际化I/O,以及预先定义好的对象cin ,cout,ceer,clog
- 国际化支持,包括多区域能力,像wchar_t和wstring等类型都对促进uniclde有所帮助
- 数值处理,包括复数模板和纯数值数组
- 异常阶层处理,包括base class exception及其derived class logic_error和runtime_error,以及更深继承的各个classes
- 以前版本的标准库程序也包含在内
- 智能指针:tr1::shared_ptr和tr1:;weak_ptr。
- tr1::function,此物得以表示任何可调用物(也就是任何函数或函数对象),只要其签名符合目标。
- tr1::bind,它可以和const和non-const成员函数协同运作,可以和by-reference参数协同运作,而且不需特殊协助就可以处理函数指针。它是第二代绑定工具
- hash tables,用来实现set,multiset,map,mutimap。以hash为基础的这些TR1容器内的元素并无任何可预期的次序
- 正则表达式,又称常规表示法、正规表示法,使用单个字符串来描述、匹配一系列符合某个句法规则的字符串
- tuples(变量组),pair只能持有两个对象,tr1::tuple可持有任意个数的对象
- tr1::array,大小固定,并不使用动态内存,是一个支持成员函数如begin何end的数组
- tr1::mem_fn,这是个语句构造上与成员函数指针一致的东西
- tr1::reference_wrapper,一个”让reference的行为更像对象“的设施,实际上容器只能持有对象或指针,它可以造成容器”犹如持有引用“
- 随机数生成工具
- 数学特殊函数,包括Laguerre多项式、Bessel函数等
- C99兼容扩充
- Type Traits,一组traits class,用以提供类型的(type)编译器信息
- tr1::result_of,这是个template,用来推导函数调用的返回类型