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) |
while ((p = malloc (size)) == 0) |
if (_callnewh(size) == 0) |
static const std::bad_alloc nomem; |
void *__CRTDECL operator new []( size_t count) _THROW1(std::bad_alloc) |
return (operator new (count)); |
void operator delete ( void * p ) |
RTCCALLBACK(_RTC_Free_hook, (p, 0)); |
void operator delete []( void * p ) |
RTCCALLBACK(_RTC_Free_hook, (p, 0)) |
看到這些CRT代碼,如何寫重載,估計你心里也已經有底了。其實CRT里還有好幾個版本的 new/delete 實現,有些是為兼容老版本,有些是Debug用的。
MFC里為調試方便,對new也進行過宏定義:
#define DEBUG_NEW new(THIS_FILE, __LINE__) |
new 的工作流程:
- 編譯器遇到 operator new 時,會去調用 type::operator new( sizeof( type ) ),如果該函數沒有沒定義,則調用 ::operator new( sizeof( type ) )。還有 placement 參數,會接在第一個參數之后。
- 分配內存空間,這時返回的指針指向的是一塊原始內存空間。
- 初始化對象,從上一步返回的地址開始初始化,系統會自動把 new-initializer 帶上,并調用相應的構造函數。
- 返回 new-type-name 或 type-name 類型的指針,用戶可以使用該指針訪問對象,該指針指向的地址也還是第2步返回的地址。
流程中還有些細節問題:
- 第1步中如果沒找到相應的函數怎么辦?
sh!t,當然編譯會出錯羅。- -
- 第2步中分配內存失敗怎么辦?
空間不足導致內存分配失敗,可以返回 NULL,或者 throw std::bad_alloc,再或者你可以調用 set_new_handler來設置一個函數,此函數將在分配內存失敗時被調用。
- 第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本身就不是一個不好的設計,這樣帶來了問題會變得復雜很多。
但是無奈,我不想改動現有的工程代碼,而且我需要統計元類型的內存分配。
理想中的設計是:
- 不允許重載全局operator new/delete。
- 使用allocator類(負責實現內存管理算法的類),可以重載或者說提供operator new/delete。
這個在STL的里有實現,為啥我不直接套用呢,因為不是所有工程都完全采用STL風格編碼,導致我只能用比較原始的方法實現。那如果遇到malloc/free呢?oh,sorry,我也無能為力了。索性C++開發人員使用new/delete來控制內存分配已經是常識了。
- operator new/delete應該有機會取得類型的元信息(如構造、析構函數、類名等),以便進行一些特殊處理(如提供debug調試信息、垃圾回收-自動完成析構等)。
如果這些信息都能獲取,OMG,那就太完美了,就能實現個強大的內存泄漏和內存管理工具。
我想實現的風格是盡可能的和普通的new/delete格式相似,并且需要能控制哪些內存分配需要被統計,哪些不要統計。存儲統計數據的變量不應是全局的,因為有時需要單獨測試某些類或模塊,所以這些統計數據應該是局部的,并可控制的。
我們可以利用 new 的 placement 來實現統計的控制,方法類似于 DEBUG_NEW 的宏定義。
比較棘手的是 delete,它的函數并沒有可傳入參數的形式。解決方法一,繼續附加信息,如magic num。解決方法二,使用全局變量控制。方法一的缺點是導致統計的內存開銷變大,而且magic num也有誤識別的可能;方法二的缺點是多線程競爭臨界資源,并且需要宏定義成函數或逗號表達式。
至于統計數據的局部化,在這方面,如果重載的不是全局new/delete,那會有更完美的解決方案。可惜現在看來全局變量的使用在所難免了,但我們可以用個全局的指針,由他指向需要被統計的變量。
1
// op_new_delete.h
2
extern size_t* _mem_use_;
3
extern 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
10
void* operator new(size_t size, int flag);
11
void* operator new[](size_t size, int flag);
12
void operator delete(void* p);
13
void operator delete[](void* p);
1
// op_new_delete.cpp
2
const size_t _magic_num_ = 0xF0F0AAAA;
3
const size_t _header_size_ = sizeof(size_t) * 2;
4
size_t* _mem_use_ = NULL;
5
size_t _mem_tmp_ = 0;
6
void* 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
}
18
void* operator new[]( size_t size, int flag )
19

{
20
return operator new(size, flag);
21
}
22
void operator delete(void* p)
23

{
24
if (p == 0) return;
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
}
32
void operator delete[](void* p)
33

{
34
operator delete(p);
35
}
當然這樣在VC2005下會有難看的 C4291 warning,只要加上與 new 相同形式的 delete 函數即可。
先寫到這吧,有不正確或更完美解決方案,就請路人留言多踩踩吧,呵呵~