MyException - 我的异常网
当前位置:我的异常网» 编程 » 定做new和delete更改内存管理方案

定做new和delete更改内存管理方案

www.MyException.Cn  网友分享于:2014-08-05  浏览:0次
定制new和delete更改内存管理方案

C++允许手工管理内存。依据使用内存的行为特征,然后修改分配和归还工作,以求获得其所建置的系统的最佳效率(包括时间和空间)。这给了程序员通过定制operatornew和operatordelete的方式来更改new与delete表达式从而更改内存管理方案的自由。但是享受这种自由的时候必须遵守一定的规范,具体可以参见《EffectiveC++ 2nd》的相关条款。本文在此基础上补充解释一些特别容易引起误解的问题。

operator new和operator delete只适合用来分配单一对象。Array所用的内存由operatornew[]分配出来,并由operatordelete[]归还。以下所有关于operatornew和operatordelete的条款也适用于operatornew[]和operatordelete[]。

当operator new抛出异常反映一个未获满足的内存需求之前,它会先调用指定的错误处理函数,一个所谓的new_handler。相关函数声明位于<new>头文件:

newspace std {
                typedefvoid (*new_handler)();
                new_handlerset_new_handler(new_handler p) throw();
}

C++并不支持class专属之new_handler.但是,你可以自己实现出这种行为。

只需令每一个class提供自己的set_new_handler和operatornew即可。其中set_new_handler供客户指定专门对应于class调用的new_handler,至于operatornew则确保在分配class对象内存的过程中以指定new_handler替换globalnew_handler。

CustomClass的operator new需要做以下事情:

1. 调用标准set_new_handler,将CustomClass的new_handler安装为globalnew_handler。

2. 调用globaloperator new,执行实际之内存分配。如果分配失败,globaloperator new会调用CutomClass的new_handler,因为那个函数刚才被安装为globalnew_handler。如果globaloperator new最终无法分配足够内存,会抛出一个bad_alloc异常.在此情况下,CustomClass的operatornew必须恢复原本的globalnew_handler,然后再传播该异常。

3. 如果globaloperator new能够分配足够的内存,CustomClass的operatornew会返回一个指针,指向分配所得。

void*CustomClass::operatornew(std::size_tsize) throw(std::bad_alloc)
{
       //安装CusomClass的new_handler.析构时恢复global new_handler。
       NewHandlerHolder h(std::set_new_handler(currentHandler));
       return ::operatornew(size);
}

复合利用这套机制的做法是建立一个baseclass,并允许derivedclasses继承单一特定能力—“设定class专属之new_handler”的能力。然后将这个baseclass转换为template,如此一来每个derivedclass将获得实体互异的classdata附件(static成员变量)。

template<typename T>       //模板用以支持class专属的set_new_handler
classNewHandlerSupport {
public:
       static std::new_handlerset_new_handler(std::new_handler p)throw();
       static void*operator new(std::size_t size)throw(std::bad_alloc);
       //...
private:
       static std::new_handlercurrentHandler;
};
 
template<typename T>
std::new_handlerNewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
       std::new_handler oldHandler = currentHandler;
       currentHandler = p;
       return oldHandler:
}
 
template<typename T>
void*NewHandlerSupport<T>::operatornew(std::size_t size) throw(std::bad_alloc)
{
       NewHandlerHodler h(std::set_new_handler(currentHandler));
       return ::operatornew(size):
}
 
//以下将每一个currentHandler初始化为null
template<typename T>
std::new_handlerNewHandlerSupport<T>::currentHandler = 0;
 
classCustomClass: publicNewHandlerSupport<CustomClass> {
       //...     //和先前一样,但不必声明set_new_handler或operator new
}

这就是CustomClass为了提供“class专属之set_new_handler”所需做的全部动作。

下面是non-member operator new伪码:

void* operatornew(std::size_tsize) throw(std::bad_alloc)
{
       using namespacestd;
       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 itmakes sense to replace new and delete.

·        用来检测运用上的错误

·        为了强化效能

·        为了收集动态分配内存之使用上的统计数据

·        为了增加分配和归还的速度

·        为了降低缺省内存管理器带来的空间额外开销

·        为了弥补缺省分配器中的非最佳齐位(alignment)

·        为了将相关对象成簇集中

·        为了获得非传统的行为

原则和方法

当你写一个placementoperator new,请确定也写出了对应的placementoperator delete如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄露。(稍后详述)

当你声明placementnew和placementdelete,请确定不要无意识地遮掩了它们的正常版本。


operatornew和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, intnLine);
void operator delete(void *p,const char*, int);

表达式new("xxx",20) CustomClass实际上就是告诉编译器调用上面的多参数operator new来分配内存。但是不要依此类推出 delete("xxx",20) pObj,这是非法的。那么怎么才能调用到这个多参数的operator delete呢?实话告诉你,你没有这个权利。呵呵,别吃惊,容我慢慢解释。

当你写一个new表达式构造对象的时候,共有两个函数被调用:一个是用以分配内存的operatornew, 一个是class的default构造函数。

假设其中第一个函数调用成功,第二个函数却抛出异常。那么步骤一的内存分配所得必须取消并恢复旧观。C++运行期系统会查找步骤一所调用的operatornew的相匹配operatordelete版本。如果找不到,运行期系统不知道如何取消并恢复原先对new的调用。于是什么也不做。

当两个operator new和operator delete有相等的参数个数,并且除了第一个参数之外其余参数的类型依次完全相同之时,我们称它们为一对匹配的operator new和operator delete。按照这个标准,上面两位就是匹配的一对了。在我们使用CustomClass*pObj = new("xxx",20) CustomClass于堆中构建一个对象的过程中,如果在执行CustomClass的构造函数时发生了异常,并且这个异常被捕获了,那么C++的异常处理机制就会自动用与被使用的operator new匹配的operator delete来释放内存(补充一点:在operatornew中抛出异常不会导致这样的动作,因为系统认为这标志着内存分配失败)。

编译期间编译器按照以下顺序寻找匹配者:首先在被构建对象类的类域中寻找,然后到父类域中,最后到全局域,此过程中一旦找到即停止搜寻并用它来生成正确的内存释放代码,如果没有找到,当发生上述异常情况时将不会有代码用来释放分配的内存,这就造成内存泄漏了。而如果一切正常,delete pObj 则总是会去调用operator delete的正规形式。  

现在明白了吧,多参数的operator delete不是给我们而是给系统调用的,它平常默默无闻,但在最危急的关头却能挺身而出,保证程序的健壮性。为了有个感性的认识,让我们看看下面的代码(试验环境是VC):

#include <malloc.h>
 
struct Base
{
    Base()
    {
        throw int(3);
    }
 
    ~Base() {}
 
    void* operatornew( size_tnSize, constchar*pFile, int lineNum)
    {
        void* p= malloc( nSize );
 
        returnp;
    }
 
    void operatordelete( void *p)
    {
        free(p);
    }
 
    void operatordelete( void* p, const char*, int)
    {
        free( p );
    }
};
 
#define NULL0
#define new new(__FILE__,__LINE__)
 
int main(void )
{
    Base* p = NULL;
 
    try
    {
        p = newBase;
 
        deletep;
    }
    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* operatornew( size_tnSize, constchar*,int)
    {
        // class Son
 
        void* p= malloc( nSize );
        returnp;
    }
 
    void operatordelete( void *p)
    {
        // class Son
        free(p);
    }
 
    void operatordelete( void* p,constchar*,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的著作。


文章评论

十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
 程序员的样子
程序员的样子
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员必看的十大电影
程序员必看的十大电影
每天工作4小时的程序员
每天工作4小时的程序员
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
老程序员的下场
老程序员的下场
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
为什么程序员都是夜猫子
为什么程序员都是夜猫子
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
编程语言是女人
编程语言是女人
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
Java程序员必看电影
Java程序员必看电影
程序员和编码员之间的区别
程序员和编码员之间的区别
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
10个调试和排错的小建议
10个调试和排错的小建议
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
中美印日四国程序员比较
中美印日四国程序员比较
我是如何打败拖延症的
我是如何打败拖延症的
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
程序员都该阅读的书
程序员都该阅读的书
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
程序员的鄙视链
程序员的鄙视链
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
总结2014中国互联网十大段子
总结2014中国互联网十大段子
鲜为人知的编程真相
鲜为人知的编程真相
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
漫画:程序员的工作
漫画:程序员的工作
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
旅行,写作,编程
旅行,写作,编程
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有