WisKeyのLullaby

huangwei.pro 『我失去了一只臂膀』「就睁开了一只眼睛」

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  12 Posts :: 0 Stories :: 23 Comments :: 0 Trackbacks

公告

“我该走哪条路?”
“这取决于你要去哪里。”
“我只想能到某个地方。”
“只要你走的够远,你始终能到达那个地方。”

Home: huangwei.pro
E-Mail: sir.huangwei [at] gmail.com
09.6 毕业于杭州电子科技大学
进入网易杭州研究院工作至今

常用链接

留言簿(1)

我参与的团队

搜索

  •  

积分与排名

  • 积分 - 51223
  • 排名 - 439

最新评论

阅读排行榜

评论排行榜

http://blog.huang-wei.com/2010/07/18/%e9%87%8d%e8%bd%bdnewdelete%e5%ae%9e%e7%8e%b0%e5%86%85%e5%ad%98%e8%ae%a1%e6%95%b0/



 

有时为了统计内存使用,或检测内存泄漏,重载全局的 new/delete 是一种比较简易的实现方法。让我们先来回顾下 new/delete 重载的相关内容吧。

技术篇

[::] new [placement] new-type-name [new-initializer]
[::] new [placement] ( type-name ) [new-initializer]
[::] delete cast-expression
[::] delete [ ] cast-expression

这里说明下,我们重载的是全局 new/delete,类 new/delete 和全局的有一些区别,在此就不细说了。

重载 operator new 的参数个数是可以任意的,只需要保证第一个参数为 size_t,其后的参数作为placement,返回类型为 void * 即可。
operator delete 的参数个数也可以是任意的,需保证第一个参数为 void *,返回类型为 void 即可。
一般的说,operator new/delete 的重载更像是函数的重载,而不是操作符的重载。

先来看看系统的new/delete都干了些啥事吧。

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
        {       // try to allocate size bytes
        void *p;
        while ((p = malloc(size)) == 0)
                if (_callnewh(size) == 0)
                {       // report no memory
                static const std::bad_alloc nomem;
                _RAISE(nomem);
                }
        return (p);
        }
 
void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
    {   // try to allocate count bytes for an array
    return (operator new(count));
    }
void operator deletevoid * p )
{
    RTCCALLBACK(_RTC_Free_hook, (p, 0));
    free( p );
}
 
void operator delete[]( void * p )
{
    RTCCALLBACK(_RTC_Free_hook, (p, 0))
    operator delete(p);
}

看到这些CRT代码,如何写重载,估计你心里也已经有底了。其实CRT里还有好几个版本的 new/delete 实现,有些是为兼容老版本,有些是Debug用的。

MFC里为调试方便,对new也进行过宏定义:

#define new DEBUG_NEW
#define DEBUG_NEW new(THIS_FILE, __LINE__)

new 的工作流程:

  1. 编译器遇到 operator new 时,会去调用 type::operator new( sizeof( type ) ),如果该函数没有没定义,则调用 ::operator new( sizeof( type ) )。还有 placement 参数,会接在第一个参数之后。
  2. 分配内存空间,这时返回的指针指向的是一块原始内存空间。
  3. 初始化对象,从上一步返回的地址开始初始化,系统会自动把 new-initializer 带上,并调用相应的构造函数。
  4. 返回 new-type-name 或 type-name 类型的指针,用户可以使用该指针访问对象,该指针指向的地址也还是第2步返回的地址。

流程中还有些细节问题:

  1. 第1步中如果没找到相应的函数怎么办?
  2. sh!t,当然编译会出错罗。- -

  3. 第2步中分配内存失败怎么办?
  4. 空间不足导致内存分配失败,可以返回 NULL,或者 throw std::bad_alloc,再或者你可以调用 set_new_handler来设置一个函数,此函数将在分配内存失败时被调用。

  5. 第3步中,如果分配成功,而初始化对象失败怎么办?
  6. 如果使用 new 运算符的 placement 形式(除了带分配大小还带参数的形式),并且对象的构造函数引发异常,编译器仍将生成调用 delete 运算符的代码;但只有当存在与分配内存的 new 运算符的 placement 形式相匹配的 delete 运算符的 placement 形式时,编译器才会这样做。如果没有实现,那可能就会造成内存泄漏。

关于内存管理的更多话题,可以进一步阅读《Effective C++》

当然,有new就应该会有delete,在这两者之间我们需要保存一些信息。现在我只是想做统计,所以内存的占用数是必须被记录的,在new时加上该值,delete时减去该值。
在申请空间时附加上一些信息域是一种较易实现的方法,当然你完全可以维护一个独立的列表,记录申请的空间,这样的实现能容纳更多的信息,适合查内存泄漏。

其实系统在new[]时,也会判断申请的对象delete时是否需要调用析构函数(1、显式的声明了析构函数;2、拥有需要调用析构函数的类的成员;3、继承自需要调用析构函数的类),如果是则会多申请4字节,保存分配的对象个数,并且返后的内存地址是实际申请得到的内存地址值加4后的结果。这也是为啥“对应的new和delete要采用相同的形式”的原因。

好像有点扯远了,成了备忘帖了。- -

利用以上的知识,就够我们实现内存计数了,so 开始动手实践吧。

设计篇

其实我认为重载全局new/delete本身就不是一个不好的设计,这样带来了问题会变得复杂很多。

但是无奈,我不想改动现有的工程代码,而且我需要统计元类型的内存分配。
理想中的设计是:

  1. 不允许重载全局operator new/delete。
  2. 使用allocator类(负责实现内存管理算法的类),可以重载或者说提供operator new/delete。
  3. 这个在STL的里有实现,为啥我不直接套用呢,因为不是所有工程都完全采用STL风格编码,导致我只能用比较原始的方法实现。那如果遇到malloc/free呢?oh,sorry,我也无能为力了。索性C++开发人员使用new/delete来控制内存分配已经是常识了。

  4. operator new/delete应该有机会取得类型的元信息(如构造、析构函数、类名等),以便进行一些特殊处理(如提供debug调试信息、垃圾回收-自动完成析构等)。
  5. 如果这些信息都能获取,OMG,那就太完美了,就能实现个强大的内存泄漏和内存管理工具。

我想实现的风格是尽可能的和普通的new/delete格式相似,并且需要能控制哪些内存分配需要被统计,哪些不要统计。存储统计数据的变量不应是全局的,因为有时需要单独测试某些类或模块,所以这些统计数据应该是局部的,并可控制的。

我们可以利用 new 的 placement 来实现统计的控制,方法类似于 DEBUG_NEW 的宏定义。

比较棘手的是 delete,它的函数并没有可传入参数的形式。解决方法一,继续附加信息,如magic num。解决方法二,使用全局变量控制。方法一的缺点是导致统计的内存开销变大,而且magic num也有误识别的可能;方法二的缺点是多线程竞争临界资源,并且需要宏定义成函数或逗号表达式。

至于统计数据的局部化,在这方面,如果重载的不是全局new/delete,那会有更完美的解决方案。可惜现在看来全局变量的使用在所难免了,但我们可以用个全局的指针,由他指向需要被统计的变量。

 1// op_new_delete.h
 2extern size_t* _mem_use_;
 3extern size_t _mem_tmp_;
 4#define SETPTR_MEM(p)    _mem_use_ = p
 5#define GETPTR_MEM        (_mem_use_ ? _mem_use_ : &_mem_tmp_)
 6#define GETCNT_MEM        *GETPTR_MEM
 7#define CLEARCNT_MEM(p)    SETPTR_MEM(p); GETCNT_MEM = 0
 8#define dbgnew            new(1)
 9#define dbgdel            delete
10void* operator new(size_t size, int flag);
11void* operator new[](size_t size, int flag);
12void operator delete(void* p);
13void operator delete[](void* p);

 1// op_new_delete.cpp
 2const size_t _magic_num_ = 0xF0F0AAAA;
 3const size_t _header_size_ = sizeof(size_t) * 2;
 4size_t* _mem_use_ = NULL;
 5size_t _mem_tmp_ = 0;
 6void* operator new( size_t size, int flag )
 7{
 8    if (size < 0) size = 0;
 9    if (flag) GETCNT_MEM += size, size += _header_size_;
10    else if (size == 0) size = 1;
11    void* p = NULL;
12    while (! p) p = ::malloc(size);
13    if (! flag) return p;
14    ((size_t*)p)[0= _magic_num_;
15    ((size_t*)p)[1= size - _header_size_;
16    return (void*)((size_t*)p + 2);
17}

18void* operator new[]( size_t size, int flag )
19{
20    return operator new(size, flag);
21}

22void operator delete(void* p)
23{
24    if (p == 0return;
25    if (_magic_num_ == ((size_t*)p)[-2]) {
26        GETCNT_MEM -= ((size_t*)p)[-1];
27        ::free((void*)((size_t*)p - 2));
28    }

29    else
30        ::free(p);
31}

32void operator delete[](void* p)
33{
34    operator delete(p);
35}

当然这样在VC2005下会有难看的 C4291 warning,只要加上与 new 相同形式的 delete 函数即可。

先写到这吧,有不正确或更完美解决方案,就请路人留言多踩踩吧,呵呵~

posted on 2010-07-23 08:48 威士忌 阅读(1304) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理