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/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
delete( void * 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 的工作流程:
<!--[if !supportLists]-->1. <!--[endif]-->编译器遇到
operator new 时,会去调用
type::operator new( sizeof( type ) ),如果该函数没有没定义,则调用
::operator new( sizeof( type ) )。还有 placement 参数,会接在第一个参数之后。
<!--[if !supportLists]-->2. <!--[endif]-->分配内存空间,这时返回的指针指向的是一块原始内存空间。
<!--[if !supportLists]-->3. <!--[endif]-->初始化对象,从上一步返回的地址开始初始化,系统会自动把
new-initializer 带上,并调用相应的构造函数。
<!--[if !supportLists]-->4. <!--[endif]-->返回
new-type-name 或
type-name 类型的指针,用户可以使用该指针访问对象,该指针指向的地址也还是第2步返回的地址。
流程中还有些细节问题:
<!--[if !supportLists]-->1. <!--[endif]-->第1步中如果没找到相应的函数怎么办?
sh!t,当然编译会出错罗。- -
<!--[if !supportLists]-->2. <!--[endif]-->第2步中分配内存失败怎么办?
空间不足导致内存分配失败,可以返回 NULL,或者 throw
std::bad_alloc,再或者你可以调用
set_new_handler来设置一个函数,此函数将在分配内存失败时被调用。
<!--[if !supportLists]-->3. <!--[endif]-->第3步中,如果分配成功,而初始化对象失败怎么办?
如果使用 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本身就不是一个不好的设计,这样带来了问题会变得复杂很多。
但是无奈,我不想改动现有的工程代码,而且我需要统计元类型的内存分配。
理想中的设计是:
<!--[if !supportLists]-->1. <!--[endif]-->不允许重载全局operator
new/delete。
<!--[if !supportLists]-->2. <!--[endif]-->使用allocator类(负责实现内存管理算法的类),可以重载或者说提供operator
new/delete。
这个在STL的里有实现,为啥我不直接套用呢,因为不是所有工程都完全采用STL风格编码,导致我只能用比较原始的方法实现。那如果遇到malloc/free呢?oh,sorry,我也无能为力了。索性C++开发人员使用new/delete来控制内存分配已经是常识了。
<!--[if !supportLists]-->3. <!--[endif]-->operator
new/delete应该有机会取得类型的元信息(如构造、析构函数、类名等),以便进行一些特殊处理(如提供debug调试信息、垃圾回收-自动完成析构等)。
如果这些信息都能获取,OMG,那就太完美了,就能实现个强大的内存泄漏和内存管理工具。
我想实现的风格是尽可能的和普通的new/delete格式相似,并且需要能控制哪些内存分配需要被统计,哪些不要统计。存储统计数据的变量不应是全局的,因为有时需要单独测试某些类或模块,所以这些统计数据应该是局部的,并可控制的。
我们可以利用 new 的 placement 来实现统计的控制,方法类似于
DEBUG_NEW 的宏定义。
比较棘手的是 delete,它的函数并没有可传入参数的形式。解决方法一,继续附加信息,如magic
num。解决方法二,使用全局变量控制。方法一的缺点是导致统计的内存开销变大,而且magic num也有误识别的可能;方法二的缺点是多线程竞争临界资源,并且需要宏定义成函数或逗号表达式。
至于统计数据的局部化,在这方面,如果重载的不是全局new/delete,那会有更完美的解决方案。可惜现在看来全局变量的使用在所难免了,但我们可以用个全局的指针,由他指向需要被统计的变量。
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
// op_new_delete.h
extern size_t* _mem_use_;
extern size_t _mem_tmp_;
#define
SETPTR_MEM(p) _mem_use_ = p
#define
GETPTR_MEM (_mem_use_ ? _mem_use_ :
&_mem_tmp_)
#define
GETCNT_MEM *GETPTR_MEM
#define CLEARCNT_MEM(p)
SETPTR_MEM(p); GETCNT_MEM = 0
#define
dbgnew new(1)
#define
dbgdel delete
void* operator new(size_t size, int flag);
void* operator new[](size_t size, int flag);
void operator
delete(void* p);
void operator
delete[](void* p);
|
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// op_new_delete.cpp
const size_t _magic_num_ = 0xF0F0AAAA;
const size_t _header_size_ = sizeof(size_t) * 2;
size_t* _mem_use_ = NULL;
size_t _mem_tmp_ = 0;
void* operator new( size_t size, int flag )
{
if (size < 0) size = 0;
if (flag) GETCNT_MEM += size, size += _header_size_;
else if (size == 0) size = 1;
void*
p = NULL;
while (! p) p = ::malloc(size);
if (! flag) return p;
((size_t*)p)[0]
= _magic_num_;
((size_t*)p)[1]
= size - _header_size_;
return (void*)((size_t*)p + 2);
}
void* operator new[]( size_t size, int flag )
{
return operator new(size, flag);
}
void operator
delete(void* p)
{
if (p == 0) return;
if (_magic_num_ == ((size_t*)p)[-2]) {
GETCNT_MEM
-= ((size_t*)p)[-1];
::free((void*)((size_t*)p
- 2));
}
else
::free(p);
}
void operator
delete[](void* p)
{
operator
delete(p);
}
|
当然这样在VC2005下会有难看的 C4291 warning,只要加上与 new 相同形式的
delete 函数即可。
先写到这吧,有不正确或更完美解决方案,就请路人留言多踩踩吧,呵呵~
分享到:
相关推荐
例如,我们可以创建一个全局的分配器类,包含一个静态成员变量来记录分配的内存总数,并在`new`和`delete`操作中更新这个计数。 ```cpp class CustomAllocator { public: void* operator new(size_t size) { void...
在描述的案例中,我们可能使用的是动态分析的一种,即通过重载全局或局部的`new`和`delete`运算符来追踪内存分配与释放。 首先,我们要理解`new`和`delete`运算符的作用。`new`用于动态分配内存,而`delete`用于...
- **全局重载`new`和`delete`**:通过重载全局的`new`和`delete`操作符,我们可以自动记录每个通过`new`分配的内存块的引用计数,并在`delete`时检查该计数。只有当引用计数降至零时,才真正释放内存。 - **...
通过对new和delete操作符的全局重载,我们可以实现对分配的内存进行引用计数,当引用计数归零时,意味着没有指针指向这块内存,此时可以安全地释放它。这里引入了CPtrManager类来管理这些计数,包括添加、减少计数...
本节将详细介绍一种基于灵巧指针的内存管理方案,该方案的核心是`CPtrManager`类,它可以追踪每个通过全局重载的`new`操作符分配的内存块,并维护这些内存块的引用计数。 1. **全局重载new和delete操作符** - `...
在这个解决方案中,通过全局重载`operator new`和`operator delete`,我们可以跟踪每个用`new`分配的内存的引用计数。`CPtrManager`类用于管理这些引用计数,它维护了一个内存块的标记,以及分配和释放内存的方法。...
为了实现这个机制,我们需要重载new和delete运算符,以便在分配和释放内存时更新引用计数。这里定义了一个名为CPtrManager的类,它负责维护内存块的引用计数和生命周期。CPtrManager提供了几个关键方法: 1. `...
此外,通过全局重载`new`和`delete`操作符,可以实现自定义的内存管理策略,如引用计数。`CPtrManager`类就是这样一个例子,它负责跟踪内存块的引用计数,当引用计数归零时执行释放操作。`CPtrManager`通过`...
所有这些状态类都从CNoTrackObject派生,这个基类提供了内存管理功能,重载了new和delete操作符,以实现低层内存分配。 CNoTrackObject类还提供了调试版本的new操作符,允许在调试模式下指定分配内存的位置和大小,...
由于C++不提供内置的垃圾回收机制,开发者需要手动管理内存,通过new和delete操作来分配和释放内存。然而,这种做法可能导致内存泄漏、悬挂指针等问题。为了解决这些问题,我们可以设计一个“垃圾回收站”来模拟类似...
另外,在内存管理方面,C++提供了`new`和`delete`操作符,这比C中的`malloc`和`free`更加方便和安全。同时,C++有更强大的标准库支持,比如STL。 #### 5. C与C++有何联系? C++最初是为了扩展C语言的功能而设计的...
这意味着在构造时可能需要使用`new`操作符,而在对象不再需要时使用`delete`来释放内存,防止内存泄漏。 4. **对象状态**:在类中,可以使用整型变量来表示对象的状态,如号码的使用状态(在用、未用、停用)。通过...
- `new[]` 和 `delete[]` 专门用于处理数组的动态内存分配与释放。 #### 十二、运算符重载 1. **运算符重载的意义**: - 运算符重载可以扩展内置运算符的功能,使其适用于用户自定义类型。 - 通过重载运算符,...
放置删除(Placement Delete/New) 放置删除和放置new是在C++中用于在预分配的内存中构造和销毁对象的运算符。它们允许更细粒度的内存管理和优化。 #### 67. 指针(Pointer) 指针是在C++中用于存储内存地址的变量...
C++支持多种运算符,如算术运算符、关系和逻辑运算符、赋值运算符、自增和自减运算符、条件运算符、sizeof运算符、new和delete运算符等,它们用于构建表达式,进行各种计算和操作。 #### 2.4 语句 C++中的语句用于...
智能指针是一种封装了原始指针功能的对象,它通过重载运算符等技术,使得程序员可以像操作普通指针一样操作智能指针,但同时又能提供额外的功能,如自动管理对象生命周期、避免内存泄漏等。常见的智能指针包括`std::...