虽然C++标准库已经为我们提供了new与delete操作符的标准实现,但是由于缺乏对具体对象的具体分析,系统默认提供的分配器在时间和空间两方面都存在着一些问题:分配器速度较慢,而且在分配小型对象时空间浪费比较严重,特别是在一些对效率或内存有较大限制的特殊应用中。比如说在嵌入式的系统中,由于内存限制,频繁地进行不定大小的内存动态分配很可能会引起严重问题,甚至出现堆破碎的风险;再比如在游戏设计中,效率绝对是一个必须要考虑的问题,而标准new与delete操作符的实现却存在着天生的效率缺陷。此时,我们可以求助于new与delete操作符的重载,它们给程序带来更灵活的内存分配控制。除了改善效率,重载new与delete还可能存在以下两点原因:
检测代码中的内存错误。
获得内存使用的统计数据。
相对于其他的操作符,operator new具有一定的特殊性,在多个方面上与它们大不相同。首先,对于用户自定义类型,如果不重载,其他操作符是无法使用的,而operator new则不然,即使不重载,亦可用于用户自定义类型。其次,在参数方面,重载其他操作符时参数的个数必须是固定的,而operator new的参数个数却可以是任意的,只需要保证第一个参数为size_t类型,返回类型为void *类型即可。所以operator new的重载会给我们一种错觉:它更像是一个函数重载,而不是一个操作符重载。
关于operator new重载函数的形式,在C++标准中有如下规定:
分配函数应当是一个类的成员函数或者是全局函数;如果一个分配函数被放于非全局名空间中,或者是在全局名空间被声明为静态,那这个程序就是格式错误的。
也就是说,重载的operator new必须是类成员函数或全局函数,而不可以是某一名空间之内的函数或是全局静态函数。此外,还要多加注意的是,重载operator new时需要兼容默认的 operator new的错误处理方式,并且要满足C++的标准规定:当要求的内存大小为0 byte时也应该返回有效的内存地址。
所以,全局的operator new重载应该不改变原有签名,而是直接无缝替换系统原有版本,如下所示:
- void * operator new(size_t size)
- {
- if(size == 0)
- size = 1;
- void *res;
- for(;;)
- {
- //allocate memory block
- res = heap_alloc(size);
- //if successful allocation, return pointer to memory
- if(res)
- break;
- //call installed new handler
- if (!CallNewHandler(size))
- break;
- //new handler was successful -- try to allocate again
- }
- return res;
- }
如果是用这种方式进行的重载,再使用时就不需要包含new头文件了。“性能优化”时通常采用这种方式。
如果重载了一个operator new,记得一定要在相同的范围内重载operator delete。因为你分配出来的内存只有你自己才知道应该如何释放。如果你偷懒或者是忘记了,编译器就会求助于默认的operator delete,用默认方式释放内存。虽然程序编译可以通过,但是这将导致惨重的代价。所以,你必须时刻记得在写下operator new的同时写下operator delete。相对于operator new,重载operator delete要简单许多,如下所示:
- void operator delete(void* p)
- {
- if(p==NULL)
- return;
- free(p);
- }
唯一要注意的一点就是,须遵循C++标准中要求删除一个NULL指针是安全的这一规定。在全局空间中重载void * operator new(size_t size)函数将会改变所有默认的operator new的行为方式,所以一定要小心使用。
如果使用不同的参数类型重载operator new/delete,则请采用如下函数声明形式:
调用时采用以下方式:
- //返回的指针必须能被普通的 ::operator delete(void*) 释放
- void* operator new(size_t size, const char* file, int line);
- //析构函数抛异常时被调用
- void operator delete(void* p, const char* file, int line);
- string* pStr = new (__FILE, __LINE__) string;
这样就能跟踪内存分配的具体位置,定位这个动作发生在哪个文件的哪一行代码中了。在“检测内存错误”和“统计内存使用数据”时通常会用这种方式重载。
此外,我们还可以为operator new的重载使用参数默认值,甚至是不定参数。其原则和普通函数重载一样。
但是在使用全局重载时应该慎之又慎,因为这样做非常具有侵略性:这会让使用你编写的库的人没有选择的余地;同时,如果两个lib中都对operator new进行了重载,在使用时会出现这样的错误:duplicated symbol link error。这是多么令人恼火的一件事啊。
与全局 ::operator new() 不同,具体类的operator new与 delete的影响面要小得多,它只影响本class及其派生类。为某个class重载operator new时必须将其定义为类的静态函数。因为operator new是在类的具体对象被构建出来之前调用的,在调用operator new的时候this指针尚未诞生,因此重载的 operator new必须是static的:
- class B
- {
- public:
- static void * operator new(size_t size);
- static void operator delete(void *p);
- // other members
- };
- void *B::operator new(size_t size)
- {
- ...
- }
- void B::operator delete(void *p)
- {
- ...
- }
当然,同全局operator new重载一样,在类中重载成员operator new也可以添加额外的参数,并且可以使用默认值。另外,成员operator new也是可以继承的。但类中的operator delete也必须声明为静态函数。因为调用operator delete时,对象已经被析构,this指针业已灰飞烟灭。
虽然为单独的class重载成员operator new/ delete是可行的,但不推荐使用。因为既然对它们进行了重载,说明它的内存分配策略已被进行了精心的特殊定制,从类似ClassName * p =new ClassName形式的代码中我们根本不能获得此信息。而且,我们有更加简单明了的Factory方案可以使用:
- static ClassName* ClassName::CreateObject();
清晰明确优于模糊不清(Explicit is better than implicit),对此我深信不疑。
关于operator new/operator delete的重载,还有一个必须小心的问题,那就是在内存分配机制中必须要考虑对象数组内存分配这一点。C++将对象数组的内存分配看作是一个不同于单个对象内存分配的单独操作。对于多数的C++实现,因为需要额外存储对象数量,new[]操作符中的个数参数会是数组的大小加上存储对象数目的一些字节。所以,如果希望改变对象数组的分配方式,同样需要重载new[]和 delete[]操作符。
请记住:
通过重载operator new 和operator delete的方法,可以自由地采用不同的分配策略,从不同的内存池中分配不同的类对象。但是是否选择重载operator new/delete一定要深思熟虑。