在c++中,new作为一个操作符,也是可以被重载的,这个可能很多人比较陌生。在 Effective C++这本书中,专门提到了这方面的知识,看过此书,做一些总结,顺便在网上找到一些内容,实现一个可以检测内存泄露的内存分配机制(new delete)。
在铺叙重载new之前,先说一下new_handler, 如果读过 windows结构化异常,对这个机制应该并不陌生,在windows结构化异常中,当出现未捕捉的异常出现时候,有一个函数handler用于处理这种默认情况,同时这个函数是可以替换成自己写的版本。
new_handler也是一样,当调用new的时候,不能成功分配内存的时候,就会调用new_handler
声明形式如下:
namespace std
{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) thow();
}
其中new_handler就是一个返回值和参数都是空的函数,set_new_handler()设置一个新的handler的同时,返回旧版本的handler。
当new分配空间失败的时候,就会调用这个函数,就好像求助的感觉一样,既然有求助的意味,所以new_handler一般要实现如下一些功能:
1. 让更多内存被使用, 即有可以释放内存的功能
2. 安装另一个new_handler,如果当前的handler无法得到更多内存,则要考虑更换handler,使得在下次调用的时候,用新的handler来处理内存释放。
3. 卸除new_handler,也就是将null传递给 set_new_handler方法,如果当前没有handler,那么new调用失败会抛出异常
4. 不返回,通常调用 abort, exit.
首先说一下重载new的必要性,
1. 用来检测运用上的错误,例如内存泄露,多次删除一个指针等,最后我们将给出一个实现检测内存泄露的new
2. 强化效能。因为自带的new函数是一个行为比较中庸的函数,对于大内存,小内存,多线程都考虑。所以自然效率会低一些,所以我们可以根据自己的需求制定new,例如在完全单线程的情况下不用考虑线程安全的问题。
3. 为了收集使用上的统计数据。在实现一个真正使用自己程序的new和delete之前,应该搜集一些程序使用动态内存的特点,例如大小,寿命等。这个时候可以重载new 来实现这些功能。
这个提法主要是为了和class new 做区分而用的。
假设现在要实现这样功能的new,如果要分配内存是 size,实际分配内存要比size大两个字节,然后分别在分配好的内存前后多四个字节,存储一个特殊的内容,作为签名。 在delete的时候,可以检测者 多分配的,以判断程序运行期间,是否对对这段内存的前后进行非法访问(underruns 和 overruns)
#include "Signew.h" const int signiture = 0xDEADBEEF; typedef unsigned char Byte; void *operator new(std::size_t size) throw (std::bad_alloc) //重载 new,当没有内存可以分配的时候,抛出bad_alloc异常 { using namespace std; size_t realSize = size + 2 * sizeof(int); //比实际size 多分配内存,作为签名所用 void *pMem = malloc(realSize); if(!pMem) throw bad_alloc(); *(static_cast<int*>(pMem)) = signiture; //在之前存储签名 *(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signiture; //在之后存储签名,要做指针类型转换,实现 +- return static_cast<Byte*>(pMem) + sizeof(int); //返回实际可用内存 }
当然可以专门为class实现一个new操作,而不是用全局的new来实例化一个对象。
下面的实现就是一个例子:
#ifndef WIDGET_H #define WIDGET_H #include <new> namespace Test { class Widget { private: int sx,sy; int ex,ey; public: Widget() { sx = sy = ex = ey = 1; } static void set_new_handler(new_handler t);//用来指定当前的 new_handler,然后保存在静态成员中 static void * operator new(size_t size); //重载new static new_handler s_originalnewhandler; //静态成员,保存 new_handler static void myhandler(void); }; class NewHandlerHolder //这是一个资源管理类,在构造的时候设置一个新的 new_handler, 在析构的时候还原回来,这样做的好处,主要是做到异常安全。 { //也就是说,无论是否因为抛出异常而终止程序,始终都会因为析构函数的调用,而将new_handler还原回来 public: NewHandlerHolder(new_handler handler) { m_original = std::set_new_handler(handler); //设置新的new_handler并且保存旧的 } ~NewHandlerHolder() { std::set_new_handler(m_original); //析构的时候,还原旧的 new_handler } private: new_handler m_original; }; } #endif
#include "Widget.h" #include <iostream> using namespace std; namespace Test { new_handler Widget::s_originalnewhandler = 0; void Widget::set_new_handler(new_handler t) { s_originalnewhandler = t; } void Widget::myhandler() //其实这个handler就是一个toy { cout<<"Can't get the memory"<<endl; } void* Widget::operator new(size_t size) // { NewHandlerHolder handler(s_originalnewhandler); //换handler return ::operator new(size);//实际分配空间 } }//namespace test;
如果对于widget类,有一个子类定义如下;
class Direved:public Widget { private: int cc; int dd; public: };如果我们使用
Direved = new Direved()
这个时候子类使用的是从父类继承过来的new operator,所以如果我们要在父类的new中做一些 特别的事情,例如多分配一些内存,然后存储一些特别的数值之类的。
所以做如下检测是必要的:
void* Widget::operator new(size_t size) { NewHandlerHolder handler(s_originalnewhandler); if(size == sizeof(Widget)) { //特殊的处理逻辑 } return ::operator new(size); }
最后介绍一个比较有趣的,也是十分实用的一种重载new的用途: 实现具有检测内存泄露的new。
主要思想就是,在每次调用new的时候,我们将 分配出来的指针值,文件(用 __FILE__获取),调用行号( 用 __LINE__获取), 保存在一个表中
每次调用delete的时候,首先用这个指针查表,如果在表中,就将表项删除,如果不在表中认为是删除未知指针。
在程序退出之前,检查这个表,如果表中依然有没有删除的项,我们认为存在new的数据,没有delete,即内存泄露。
代码如下:
#ifndef MEM_CHECK_NEW_H #define MEM_CHECK_NEW_H #ifdef _DEBUG void *operator new(size_t size, const char *file_name, long line); //重载new, 第二个,第三个参数是文件名,和行号 void *operator new[](size_t size, const char *file_name, long line);//重载new[] void operator delete(void *);//重载delete void operator delete[](void *); //#define new new(__FILE__,__LINE__) #endif #endif
实现部分
#include "mem_check_new.h" #include <malloc.h> #include <stdio.h> //#include <iostream> //using namespace std; #ifdef _DEBUG namespace { struct info //定义表中存储的数据,分配的指针值,文件名,行号 { void *ptr; const char *file_name; long line; }; info ptr_list[1024]; //内存分配表 unsigned int ptrn = 0;//内存分配表项 int find_ptr(void *p) //依据指针,查询记录这个分配的表项 { for(unsigned int i = 0; i < ptrn; i++) { if(ptr_list[i].ptr == p) { return i; } } return -1; } void del_ptr(unsigned int i) //依据表项id,删除这个分配记录 { while(i+1 < ptrn) { ptr_list[i] = ptr_list[i+1]; i++; } ptrn--; } class process_end //在程序退出前,检测是否有泄露 { public: ~process_end() { for(unsigned int i = 0; i < ptrn; i++) { printf("file: %s, line: %d Memory leak\n", ptr_list[i].file_name, ptr_list[i].line); } } }; process_end pe; //用一个全局对象,在程序退出前,析构这个对象,然后检测开始 }//end of namespace non; //void* operator new(size_t size, const char *file_name,long line) void* operator new(size_t size, const char *file_name,long line) //重载new,每次分配内存,都放入表中记录下来 { void *p = malloc(size); ptr_list[ptrn].ptr = p; ptr_list[ptrn].file_name = file_name; ptr_list[ptrn].line = line; ptrn++; return p; } void *operator new[](size_t size, const char *file_name, long line) //重载 new[] { return operator new(size, file_name, line); } void operator delete(void *p) //调用delete的时候,首先在表中查找当前指针的值,如果存在就删除 { int i = find_ptr(p); if(i >= 0) { free(p); del_ptr(i); } else { printf("delete unknown pointer\n"); } } void operator delete[](void *p) { operator delete(p); } #endif