C++允许手工管理内存。依据使用内存的行为特征,然后修改分配和归还工作,以求获得其所建置的系统的最佳效率(包括时间和空间)。这给了程序员通过定制operator new和operator delete的方式来更改new与delete表达式从而更改内存管理方案的自由。但是享受这种自由的时候必须遵守一定的规范,具体可以参见《EffectiveC++ 2nd》的相关条款。本文在此基础上补充解释一些特别容易引起误解的问题。
operator new和operator delete只适合用来分配单一对象。Array所用的内存由operator new[]分配出来,并由operator delete[]归还。以下所有关于operator new和operator delete的条款也适用于operator new[]和operator delete[]。
当operator new抛出异常反映一个未获满足的内存需求之前,它会先调用指定的错误处理函数,一个所谓的new_handler。相关函数声明位于<new>头文件:
newspace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
C++并不支持class专属之new_handler.但是,你可以自己实现出这种行为。
只需令每一个class提供自己的set_new_handler和operator new即可。其中set_new_handler供客户指定专门对应于class调用的new_handler,至于operator new则确保在分配class对象内存的过程中以指定new_handler替换global new_handler。
CustomClass的operator new需要封装实现以下事情:
1. 调用标准set_new_handler,将CustomClass的new_handler安装为global new_handler。
2. 调用global operator new,执行实际之内存分配。如果分配失败,global operator new会调用CutomClass的new_handler,因为那个函数刚才被安装为global new_handler。如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常.在此情况下,确保CustomClass的 operator new 必须恢复原本的 global new_handler,然后再传播该异常。
3. 如果global operator new能够分配足够的内存,CustomClass的operator new会返回一个指针,指向分配所得。
void* CustomClass::operator new(std::size_t size) throw(std::bad_alloc)
{
//安装CusomClass的new_handler.析构时恢复global new_handler。
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
复合利用这套机制的做法是建立一个base class,并允许derived classes继承单一特定能力—“设定class专属之new_handler”的能力。然后将这个base class转换为template,如此一来每个derived class将获得实体互异的class data附件(static成员变量)。
template<typename T> //模板用以支持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);
//...
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)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size):
}
//以下将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
class CustomClass : public NewHandlerSupport<CustomClass> {
//... //和先前一样,但不必声明set_new_handler或operator new
}
这就是CustomClass为了提供“class专属之set_new_handler”所需做的全部动作。
下面是non-member operator new伪码:
void* operator new(std::size_tsize) throw(std::bad_alloc)
{
using namespace std;
if(size==0) {
size = 1;
}
while(true){
//尝试分配size bytes;
if(分配成功)
return (一个指针,指向分配得来的内存);
//分配失败:找出目前的new_handling函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)(*globalHandler)();
else throw std::bad_alloc();
}
}
Understand when it makes sense to replace new and delete.
· 用来检测运用上的错误
· 为了强化效能
· 为了收集动态分配内存之使用上的统计数据
· 为了增加分配和归还的速度
· 为了降低缺省内存管理器带来的空间额外开销
· 为了弥补缺省分配器中的非最佳齐位(alignment)
· 为了将相关对象成簇集中
· 为了获得非传统的行为
原则和方法
当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄露。(稍后详述)
当你声明placement new和placement delete,请确定不要无意识地遮掩了它们的正常版本。
operator new和operator delete都有其正规形式(normal signature ):
void* operator new(size_t size);
void operator delete(void *p);
void operator delete(void *p, size_t size);
数组版本则是:
void* operator new[](size_t size);
void operator delete[](void *p);
void operator delete[](void *p, size_t size);
普通的new与delete表达式在分配与释放内存时调用的就是这些正规形式的版本。operator delete(void *p, size_t size)中的第二个参数由系统自动传递,用来表明指针所指对象的实际大小。一般来说operator delete(void*)的优先级比operator delete(void*, size_t)要高,这意味着如果在同一空间(scope)定义了这两种形式的delete,拥有单一参数者优先被编译器选择。这一点在VC中得到验证,不知其它编译器如何?
除了上面的正规形式外,我们还可以定义拥有更多参数的operator new和operator delete,只要保证前者的返回值和第一个参数分别是void*和size_t类型,而后者的分别是void和void*就行了。比如:
void* operator new(size_t size, const char* szFile, int nLine);
void operator delete(void *p, const char*, int);
表达式new("xxx",20) CustomClass实际上就是告诉编译器调用上面的多参数operator new来分配内存。但是不要依此类推出 delete("xxx",20) pObj,这是非法的。那么怎么才能调用到这个多参数的operator delete呢?实话告诉你,你没有这个权利。呵呵,别吃惊,容我慢慢解释。
当你写一个new表达式构造对象的时候,共有两个函数被调用:一个是用以分配内存的operator new, 一个是class的default构造函数。
假设其中第一个函数调用成功,第二个函数却抛出异常。那么步骤一的内存分配所得必须取消并恢复旧观。C++运行期系统会查找步骤一所调用的operator new的相匹配operator delete版本。如果找不到,运行期系统不知道如何取消并恢复原先对new的调用。于是什么也不做。
当两个operator new和operator delete有相等的参数个数,并且除了第一个参数之外其余参数的类型依次完全相同之时,我们称它们为一对匹配的operator new和operator delete。按照这个标准,上面两位就是匹配的一对了。在我们使用CustomClass* pObj = new("xxx",20) CustomClass于堆中构建一个对象的过程中,如果在执行CustomClass的构造函数时发生了异常,并且这个异常被捕获了,那么C++的异常处理机制就会自动用与被使用的operator new匹配的operator delete来释放内存(补充一点:在operator new中抛出异常不会导致这样的动作,因为系统认为这标志着内存分配失败)。编译期间编译器按照以下顺序寻找匹配者:首先在被构建对象类的类域中寻找,然后到父类域中,最后到全局域,此过程中一旦找到即停止搜寻并用它来生成正确的内存释放代码,如果没有找到,当发生上述异常情况时将不会有代码用来释放分配的内存,这就造成内存泄漏了。而如果一切正常,delete pObj 则总是会去调用operator delete的正规形式。
现在明白了吧,多参数的operator delete不是给我们而是给系统调用的,它平常默默无闻,但在最危急的关头却能挺身而出,保证程序的健壮性。为了有个感性的认识,让我们看看下面的代码(试验环境是VC):
#include <malloc.h>
struct Base
{
Base()
{
throw int(3);
}
~Base() {}
void* operator new(size_t nSize, const char* pFile, int lineNum)
{
void* p= malloc(nSize);
return p;
}
void operator delete(void *p)
{
free(p);
}
void operator delete(void* p, const char*, int)
{
free( p );
}
};
#define NULL 0
#define new new(__FILE__,__LINE__)
int main(void )
{
Base* p = NULL;
try
{
p = new Base;
delete p;
}
catch(...)
{
}
return 0;
}
跟踪执行会发现:程序在 p = new Base 处抛出一个异常后马上跳去执行operator delete(void*,const char*,int)。注释掉Base构造函数中的throw int(3)重来一遍,则new成功,然后执行delete p,这时实际调用的是Base::operator delete(void*)。以上试验结果符合我们的预期。注意,operator new和operator delete是可以被继承和重定义的,那接下来就看看它们在继承体系中的表现。引进一个Base的派生类(代码加在#define NULL 0的前面):
struct Son : public Base
{
Son()
{
}
void* operator new(size_t nSize, const char*, int)
{
// class Son
void* p = malloc(nSize);
return p;
}
void operator delete(void *p)
{
// class Son
free(p);
}
void operator delete(void* p, const char*, int)
{
// class Son
free( p );
}
};
然后将main函数中的p = new Base改成p = new Son并且取消对Base()中的throw int(3)的注释,跟踪执行,发现这回new表达式调用的是Son重定义的operator new,抛出异常后也迅速进入了正确的operator delete,即Son重定义的多参数版本。一切都如所料,是吗?呵呵,别急着下结论,让我们把抛异常的语句注释掉再跑一次吧。很明显,有些不对劲。这次delete p没有如我们所愿去调用Son::operator delete(void*),而是找到了在Base中定义的版本。怎么回事?我愿意留一分钟让好奇的你仔细想想。
找到答案了吗?没错,罪魁祸首就是那愚蠢的Base析构函数声明。作为一个领导着派生类的基类,析构函数竟然不声明成virtual函数,这简直就是渎职。赶紧纠正,在~Base()前加上一个神圣的virtual,rebuild and run.....。谢天谢地,世界终于完美了。
可能你会疑惑,在没有给基类析构函数加virtual之前,当发生异常时C++为什么知道正确地调用派生类定义的多参数operator delete,而不是基类的?其实很简单,new一个对象时必须提供此对象的确切类型,所以编译器能够在编译期确定new表达式抛出异常后应该调用哪个类定义的operator delete。对于正常的delete p来说,如果p被声明为非基类类型的指针,编译器就会在编译时决定调用这种声明类型定义的operator delete(静态绑定),而如果p是某种基类类型指针,编译器就会聪明地把到底调用哪个类定义的operator delete留待运行期决定(动态绑定)。那么编译器如何判断p是否是基类指针呢?实际上它的根据就是p的声明类型中定义的析构函数(RTTI),只有在析构函数是虚拟的情况下p才被看成基类指针。这就可以解释上面碰到的问题。当时 p 被声明为 Base*,程序中它实际指向一个Son对象,但我们并没有把~Base()声明为虚拟的,所以编译器大胆地帮我们做了静态绑定,也即生成调用Base::operator delete(void*)的代码。不过千万不要以为所有编译器都会这样做,以上分析仅仅是基于VC在本次试验中的表现。事实上在C++标准中,经由一个基类指针删除一个派生类对象,而此基类却有一个非虚拟的析构函数,结果未定义。明白了吧老兄,编译器在这种情况下没有生成引爆你电脑的代码已经算是相当客气与负责了。现在你该能够体会Scott Meyers劝你"总是让base class拥有virtual destructor"时的苦心吧。
最后要指出的是,试验代码中对operator new和operator delete的实现相当不规范,负责任的做法仍然请大家参考Scott Meyers的著作。