国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
一個跨平臺的 C 內(nèi)存泄漏檢測器
2004 年 3 月 01 日
內(nèi)存泄漏對于C/C++程序員來說也可以算作是個永恒的話題了吧。在Windows下,MFC的一個很有用的功能就是能在程序運行結(jié)束時報告是否發(fā)生了內(nèi)存泄漏。在Linux下,相對來說就沒有那么容易使用的解決方案了:像mpatrol之類的現(xiàn)有工具,易用性、附加開銷和性能都不是很理想。本文實現(xiàn)一個極易于使用、跨平臺的C++內(nèi)存泄漏檢測器。并對相關(guān)的技術(shù)問題作一下探討。
對于下面這樣的一個簡單程序test.cpp:
int main() { int* p1 = new int; char* p2 = new char[10]; return 0; }
我們的基本需求當然是對于該程序報告存在兩處內(nèi)存泄漏。要做到這點的話,非常簡單,只要把debug_new.cpp也編譯、鏈接進去就可以了。在Linux下,我們使用:
g++ test.cpp debug_new.cpp -o test
輸出結(jié)果如下所示:
Leaked object at 0x805e438 (size 10, <Unknown>:0) Leaked object at 0x805e410 (size 4, <Unknown>:0)
如果我們需要更清晰的報告,也很簡單,在test.cpp開頭加一行
#include "debug_new.h"
即可。添加該行后的輸出如下:
Leaked object at 0x805e438 (size 10, test.cpp:5) Leaked object at 0x805e410 (size 4, test.cpp:4)
非常簡單!
回頁首
在new/delete操作中,C++為用戶產(chǎn)生了對operator new和operator delete的調(diào)用。這是用戶不能改變的。operator new和operator delete的原型如下所示:
void *operator new(size_t) throw(std::bad_alloc); void *operator new[](size_t) throw(std::bad_alloc); void operator delete(void*) throw(); void operator delete[](void*) throw();
對于"new int",編譯器會產(chǎn)生一個調(diào)用"operator new(sizeof(int))",而對于"new char[10]",編譯器會產(chǎn)生"operator new[](sizeof(char) * 10)"(如果new后面跟的是一個類名的話,當然還要調(diào)用該類的構(gòu)造函數(shù))。類似地,對于"delete ptr"和"delete[] ptr",編譯器會產(chǎn)生"operator delete(ptr)"調(diào)用和"operator delete[](ptr)"調(diào)用(如果ptr的類型是指向?qū)ο蟮闹羔樀脑挘窃趏perator delete之前還要調(diào)用對象的析構(gòu)函數(shù))。當用戶沒有提供這些操作符時,編譯系統(tǒng)自動提供其定義;而當用戶自己提供了這些操作符時,就覆蓋了編譯系統(tǒng)提供的版本,從而可獲得對動態(tài)內(nèi)存分配操作的精確跟蹤和控制。
同時,我們還可以使用placement new操作符來調(diào)整operator new的行為。所謂placement new,是指帶有附加參數(shù)的new操作符,比如,當我們提供了一個原型為
void* operator new(size_t size, const char* file, int line);
的操作符時,我們就可以使用"new("hello", 123) int"來產(chǎn)生一個調(diào)用"operator new(sizeof(int), "hello", 123)"。這可以是相當靈活的。又如,C++標準要求編譯器提供的一個placement new操作符是
void* operator new(size_t size, const std::nothrow_t&);
其中,nothrow_t通常是一個空結(jié)構(gòu)(定義為"struct nothrow_t {};"),其唯一目的是提供編譯器一個可根據(jù)重載規(guī)則識別具體調(diào)用的類型。用戶一般簡單地使用"new(std::nothrow) 類型"(nothrow是一個nothrow_t類型的常量)來調(diào)用這個placement new操作符。它與標準new的區(qū)別是,new在分配內(nèi)存失敗時會拋出異常,而"new(std::nothrow)"在分配內(nèi)存失敗時會返回一個空指針。
要注意的是,沒有對應(yīng)的"delete(std::nothrow) ptr"的語法;不過后文會提到另一個相關(guān)問題。
要進一步了解以上關(guān)于C++語言特性的信息,請參閱[Stroustrup1997],特別是6.2.6、10.4.11、15.6、19.4.5和B.3.4節(jié)。這些C++語言特性是理解本實現(xiàn)的關(guān)鍵。
回頁首
和其它一些內(nèi)存泄漏檢測的方式類似,debug_new中提供了operator new重載,并使用了宏在用戶程序中進行替換。debug_new.h中的相關(guān)部分如下:
void* operator new(size_t size, const char* file, int line); void* operator new[](size_t size, const char* file, int line); #define new DEBUG_NEW #define DEBUG_NEW new(__FILE__, __LINE__)
拿上面加入debug_new.h包含后的test.cpp來說,"new char[10]"在預處理后會變成"new("test.cpp", 4) char[10]",編譯器會據(jù)此產(chǎn)生一個"operator new[](sizeof(char) * 10, "test.cpp", 4)"調(diào)用。通過在debug_new.cpp中自定義"operator new(size_t, const char*, int)"和"operator delete(void*)"(以及"operator new[]…"和"operator delete[]…";為避免行文累贅,以下不特別指出,說到operator new和operator delete均同時包含數(shù)組版本),我可以跟蹤所有的內(nèi)存分配調(diào)用,并在指定的檢查點上對不匹配的new和delete操作進行報警。實現(xiàn)可以相當簡單,用map記錄所有分配的內(nèi)存指針就可以了:new時往map里加一個指針及其對應(yīng)的信息,delete時刪除指針及對應(yīng)的信息;delete時如果map里不存在該指針為錯誤刪除;程序退出時如果map里還存在未刪除的指針則說明有內(nèi)存泄漏。
不過,如果不包含debug_new.h,這種方法就起不了作用了。不僅如此,部分文件包含debug_new.h,部分不包含debug_new.h都是不可行的。因為雖然我們使用了兩種不同的operator new --"operator new(size_t, const char*, int)"和"operator new(size_t)"-- 但可用的"operator delete"還是只有一種!使用我們自定義的"operator delete",當我們刪除由"operator new(size_t)"分配的指針時,程序?qū)⒄J為被刪除的是一個非法指針!我們處于一個兩難境地:要么對這種情況產(chǎn)生誤報,要么對重復刪除同一指針兩次不予報警:都不是可接受的良好行為。
看來,自定義全局"operator new(size_t)"也是不可避免的了。在debug_new中,我是這樣做的:
void* operator new(size_t size) { return operator new(size, "<Unknown>", 0); }
但前面描述的方式去實現(xiàn)內(nèi)存泄漏檢測器,在某些C++的實現(xiàn)中(如GCC 2.95.3中帶的SGI STL)工作正常,但在另外一些實現(xiàn)中會莫名其妙地崩潰。原因也不復雜,SGI STL使用了內(nèi)存池,一次分配一大片內(nèi)存,因而使利用map成為可能;但在其他的實現(xiàn)可能沒這樣做,在map中添加數(shù)據(jù)會調(diào)用operator new,而operator new會在map中添加數(shù)據(jù),從而構(gòu)成一個死循環(huán),導致內(nèi)存溢出,應(yīng)用程序立即崩潰。因此,我們不得不停止使用方便的STL模板,而使用手工構(gòu)建的數(shù)據(jù)結(jié)構(gòu):
struct new_ptr_list_t { new_ptr_list_t* next; const char* file; int line; size_t size; };
我最初的實現(xiàn)方法就是每次在使用new分配內(nèi)存時,調(diào)用malloc多分配 sizeof(new_ptr_list_t) 個字節(jié),把分配的內(nèi)存全部串成一個一個鏈表(利用next字段),把文件名、行號、對象大小信息分別存入file、line和size字段中,然后返回(malloc返回的指針 + sizeof(new_ptr_list_t))。在delete時,則在鏈表中搜索,如果找到的話((char*)鏈表指針 + sizeof(new_ptr_list_t) == 待釋放的指針),則調(diào)整鏈表、釋放內(nèi)存,找不到的話報告刪除非法指針并abort。
至于自動檢測內(nèi)存泄漏,我的做法是生成一個靜態(tài)全局對象(根據(jù)C++的對象生命期,在程序初始化時會調(diào)用該對象的構(gòu)造函數(shù),在其退出時會調(diào)用該對象的析構(gòu)函數(shù)),在其析構(gòu)函數(shù)中調(diào)用檢測內(nèi)存泄漏的函數(shù)。用戶手工調(diào)用內(nèi)存泄漏檢測函數(shù)當然也是可以的。
基本實現(xiàn)大體就是如此。
回頁首
上述方案最初工作得相當好,直到我開始創(chuàng)建大量的對象為止。由于每次delete時需要在鏈表中進行搜索,平均搜索次數(shù)為(鏈表長度/2),程序很快就慢得像烏龜爬。雖說只是用于調(diào)試,速度太慢也是不能接受的。因此,我做了一個小更改,把指向鏈表頭部的new_ptr_list改成了一個數(shù)組,一個對象指針放在哪一個鏈表中則由它的哈希值決定。--用戶可以更改宏DEBUG_NEW_HASH和DEBUG_NEW_HASHTABLESIZE的定義來調(diào)整debug_new的行為。他們的當前值是我測試下來比較滿意的定義。
使用中我們發(fā)現(xiàn),在某些特殊情況下(請直接參看debug_new.cpp中關(guān)于DEBUG_NEW_FILENAME_LEN部分的注釋),文件名指針會失效。因此,目前的debug_new的缺省行為會復制文件名的頭20個字符,而不只是存儲文件名的指針。另外,請注意原先new_ptr_list_t的長度為16字節(jié),現(xiàn)在是32字節(jié),都能保證在通常情況下內(nèi)存對齊。
此外,為了允許程序能和 new(std::nothrow) 一起工作,我也重載了operator new(size_t, const std::nothrow_t&) throw();不然的話,debug_new會認為對應(yīng)于 new(nothrow) 的delete調(diào)用刪除的是一個非法指針。由于debug_new不拋出異常(內(nèi)存不足時程序直接報警退出),所以這一重載的操作只不過是調(diào)用 operator new(size_t) 而已。這就不用多說了。
前面已經(jīng)提到,要得到精確的內(nèi)存泄漏檢測報告,可以在文件開頭包含"debug_new.h"。我的慣常做法可以用作參考:
#ifdef _DEBUG #include "debug_new.h" #endif
包含的位置應(yīng)當盡可能早,除非跟系統(tǒng)的頭文件(典型情況是STL的頭文件)發(fā)生了沖突。在某些情況下,可能會不希望debug_new重定義new,這時可以在包含debug_new.h之前定義DEBUG_NEW_NO_NEW_REDEFINITION,這樣的話,在用戶應(yīng)用程序中應(yīng)使用debug_new來代替new(順便提一句,沒有定義DEBUG_NEW_NO_NEW_REDEFINITION時也可以使用debug_new代替new)。在源文件中也許就該這樣寫:
#ifdef _DEBUG #define DEBUG_NEW_NO_NEW_REDEFINITION #include "debug_new.h" #else #define debug_new new #endif
并在需要追蹤內(nèi)存分配的時候全部使用debug_new(考慮使用全局替換)。
用戶可以選擇定義DEBUG_NEW_EMULATE_MALLOC,這樣debug_new.h會使用debug_new和delete來模擬malloc和free操作,使得用戶程序中的malloc和free操作也可以被跟蹤。在使用某些編譯器的時候(如Digital Mars C++ Compiler 8.29和Borland C++ Compiler 5.5.1),用戶必須定義NO_PLACEMENT_DELETE,否則編譯無法通過。用戶還可以使用兩個全局布爾量來調(diào)整debug_new的行為:new_verbose_flag,缺省為false,定義為true時能在每次new/delete時向標準錯誤輸出顯示跟蹤信息;new_autocheck_flag,缺省為true,即在程序退出時自動調(diào)用check_leaks檢查內(nèi)存泄漏,改為false的話用戶必須手工調(diào)用check_leaks來檢查內(nèi)存泄漏。
需要注意的一點是,由于自動調(diào)用check_leaks是在debug_new.cpp中的靜態(tài)對象析構(gòu)時,因此不能保證用戶的全局對象的析構(gòu)操作發(fā)生在check_leaks調(diào)用之前。對于Windows上的MSVC,我使用了"#pragma init_seg(lib)"來調(diào)整對象分配釋放的順序,但很遺憾,我不知道在其他的一些編譯器中(特別是,我沒能成功地在GCC中解決這一問題)怎么做到這一點。為了減少誤報警,我采取的方式是在自動調(diào)用了check_leaks之后設(shè)new_verbose_flag為true;這樣,就算誤報告了內(nèi)存泄漏,隨后的delete操作還是會被打印顯示出來。只要泄漏報告和delete報告的內(nèi)容一致,我們?nèi)钥梢耘袛喑鰶]有發(fā)生內(nèi)存泄漏。
Debug_new也能檢測對同一指針重復調(diào)用delete(或delete無效指針)的錯誤。程序?qū)@示錯誤的指針值,并強制調(diào)用abort退出。
還有一個問題是異常處理。這值得用專門的一節(jié)來進行說明。
回頁首
我們看一下以下的簡單程序示例:
#include <stdexcept> #include <stdio.h> void* operator new(size_t size, int line) { printf("Allocate %u bytes on line %d\\n", size, line); return operator new(size); } class Obj { public: Obj(int n); private: int _n; }; Obj::Obj(int n) : _n(n) { if (n == 0) { throw std::runtime_error("0 not allowed"); } } int main() { try { Obj* p = new(__LINE__) Obj(0); delete p; } catch (const std::runtime_error& e) { printf("Exception: %s\\n", e.what()); } }
看出代碼中有什么問題了嗎?實際上,如果我們用MSVC編譯的話,編譯器的警告信息已經(jīng)告訴我們發(fā)生了什么:
test.cpp(27) : warning C4291: 'void *__cdecl operator new(unsigned int,int)' : no matching operator delete found; memory will not be freed if initialization throws an exception
好,把debug_new.cpp鏈接進去。運行結(jié)果如下:
Allocate 4 bytes on line 27 Exception: 0 not allowed Leaked object at 00342BE8 (size 4, <Unknown>:0)
啊哦,內(nèi)存泄漏了不是!
當然,這種情況并非很常見??墒牵S著對象越來越復雜,誰能夠保證一個對象的子對象的構(gòu)造函數(shù)或者一個對象在構(gòu)造函數(shù)中調(diào)用的所有函數(shù)都不會拋出異常?并且,解決該問題的方法并不復雜,只是需要編譯器對 C++ 標準有較好支持,允許用戶定義 placement delete 算符([C++1998],5.3.4節(jié);網(wǎng)上可以找到1996年的標準草案,比如下面的網(wǎng)址http://www.comnets.rwth-aachen.de/doc/c++std/expr.html#expr.new)。在我測試的編譯器中,GCC(2.95.3或更高版本,Linux/Windows)和MSVC(6.0或更高版本)沒有問題,而Borland C++ Compiler 5.5.1和Digital Mars C++ Compiler(到v8.38為止的所有版本)則不支持該項特性。在上面的例子中,如果編譯器支持的話,我們就需要聲明并實現(xiàn) operator delete(void*, int) 來回收new分配的內(nèi)存。編譯器不支持的話,需要使用宏讓編譯器忽略相關(guān)的聲明和實現(xiàn)。如果要讓debug_new在Borland C++ Compiler 5.5.1或Digital Mars C++ Compiler下編譯的話,用戶必須定義宏NO_PLACEMENT_DELETE;當然,用戶得自己注意小心構(gòu)造函數(shù)中拋出異常這個問題了。
回頁首
IBM developerWorks上刊載了洪琨先生設(shè)計實現(xiàn)的一個Linux上的內(nèi)存泄漏檢測方法([洪琨2003])。我的方案與其相比,主要區(qū)別如下:
優(yōu)點:
跨平臺:只使用標準函數(shù),并且在GCC 2.95.3/3.2(Linux/Windows)、MSVC 6、Digital Mars C++ 8.29、Borland C++ 5.5.1等多個編譯器下調(diào)試通過。(雖然Linux是我的主要開發(fā)平臺,但我發(fā)現(xiàn),有時候能在Windows下編譯運行代碼還是非常方便的。)
易用性:由于重載了operator new(size_t)--洪琨先生只重載了operator new(size_t, const char*, int)--即使不包含我的頭文件也能檢測內(nèi)存泄漏;程序退出時能自動檢測內(nèi)存泄漏;可以檢測用戶程序(不包括系統(tǒng)/庫文件)中malloc/free產(chǎn)生的內(nèi)存泄漏。
靈活性:有多個靈活的可配置項,可使用宏定義進行編譯時選擇。
可重入性:不使用全局變量,沒有嵌套delete問題。
異常安全性:在編譯器支持的情況下,能夠處理構(gòu)造函數(shù)中拋出的異常而不發(fā)生內(nèi)存泄漏。
缺點:
單線程模型:跨平臺的多線程實現(xiàn)較為麻煩,根據(jù)項目的實際需要,也為了代碼清晰簡單起見,我的方案不是線程安全的;換句話說,如果多個線程中同時進行new或delete操作的話,后果未定義。
未實現(xiàn)運行中內(nèi)存泄漏檢測報告機制:沒有遇到這個需求J;不過,如果要手工調(diào)用check_leaks函數(shù)實現(xiàn)的話也不困難,只是跨平臺性就有點問題了。
不能檢測帶 [] 算符和不帶 [] 算符混用的不匹配:主要也是需求問題(如果要修改實現(xiàn)的話并不困難)。
不能在錯誤的delete調(diào)用時顯示文件名和行號:應(yīng)該不是大問題;由于我重載了operator new(size_t),可以保證delete出錯時程序必然有問題,因而我不只是顯示警告信息,而且會強制程序abort,可以通過跟蹤程序、檢查abort時程序的調(diào)用棧知道問題出在哪兒。
另外,現(xiàn)在已存在不少商業(yè)和Open Source的內(nèi)存泄漏檢測器,本文不打算一一再做比較。Debug_new與它們相比,功能上總的來說仍較弱,但是,其良好的易用性和跨平臺性、低廉的附加開銷還是具有很大優(yōu)勢的。
回頁首
以上段落基本上已經(jīng)說明了debug_new的主要特點。下面做一個小小的總結(jié)。
重載的算符:
operator new(size_t, const char*, int)
operator new[](size_t, const char*, int)
operator new(size_t)
operator new[](size_t)
operator new(size_t, const std::nothrow_t&)
operator new[](size_t, const std::nothrow_t&)
operator delete(void*)
operator delete[](void*)
operator delete(void*, const char*, int)
operator delete[](void*, const char*, int)
operator delete(void*, const std::nothrow_t&)
operator delete[](void*, const std::nothrow_t&)
提供的函數(shù):
check_leaks()
檢查是否發(fā)生內(nèi)存泄漏
提供的全局變量
new_verbose_flag
是否在new和delete時"羅嗦"地顯示信息
new_autocheck_flag
是否在程序退出是自動檢測一次內(nèi)存泄漏
可重定義的宏:
NO_PLACEMENT_DELETE
假設(shè)編譯器不支持placement delete(全局有效)
DEBUG_NEW_NO_NEW_REDEFINITION
不重定義new,假設(shè)用戶會自己使用debug_new(包含debug_new.h時有效)
DEBUG_NEW_EMULATE_MALLOC
重定義malloc/free,使用new/delete進行模擬(包含debug_new.h時有效)
DEBUG_NEW_HASH
改變內(nèi)存塊鏈表哈希值的算法(編譯debug_new.cpp時有效)
DEBUG_NEW_HASHTABLE_SIZE
改變內(nèi)存塊鏈表哈希桶的大?。ň幾gdebug_new.cpp時有效)
DEBUG_NEW_FILENAME_LEN
如果在分配內(nèi)存時復制文件名的話,保留的文件名長度;為0時則自動定義DEBUG_NEW_NO_FILENAME_COPY(編譯debug_new.cpp時有效;參見文件中的注釋)
DEBUG_NEW_NO_FILENAME_COPY
分配內(nèi)存時不進行文件名復制,而只是保存其指針;效率較高(編譯debug_new.cpp時有效;參見文件中的注釋)
我本人認為,debug_new目前的一個主要缺陷是不支持多線程。對于某一特定平臺,要加入多線程支持并不困難,難就難在通用上(當然,條件編譯是一個辦法,雖然不夠優(yōu)雅)。等到C++標準中包含線程模型時,這個問題也許能比較完美地解決吧。另一個辦法是使用像boost這樣的程序庫中的線程封裝類,不過,這又會增加對其它庫的依賴性--畢竟boost并不是C++標準的一部分。如果項目本身并不用boost,單為了這一個目的使用另外一個程序庫似乎并不值得。因此,我自己暫時就不做這進一步的改進了。
另外一個可能的修改是保留標準operator new的異常行為,使其在內(nèi)存不足的情況下拋出異常(普通情況)或是返回NULL(nothrow情況),而不是像現(xiàn)在一樣終止程序運行(參見debug_new.cpp的源代碼)。這一做法的難度主要在于后者:我沒想出什么方法,可以保留 new(nothrow) 的語法,同時能夠報告文件名和行號,并且還能夠使用普通的new。不過,如果不使用標準語法,一律使用debug_new和debug_new_nothrow的話,那還是非常容易實現(xiàn)的。
如果大家有改進意見或其它想法的話,歡迎來信討論。
debug_new 的源代碼目前可以在dbg_new.zip處下載。
在這篇文章的寫完之后,我終于還是實現(xiàn)了一個線程安全的版本。該版本使用了一個輕量級的跨平臺互斥體類fast_mutex(目前支持Win32和POSIX線程,在使用GCC(Linux/MinGW)、MSVC時能通過命令行參數(shù)自動檢測線程類型)。有興趣的話可在http://mywebpage.netscape.com/yongweiwu/dbg_new.tgz下載。
[C++1998] ISO/IEC 14882. Programming Languages-C++, 1st Edition. International Standardization Organization, International Electrotechnical Commission, American National Standards Institute, and Information Technology Industry Council, 1998
[Stroustrup1997] Bjarne Stroustrup. The C++ Programming Language, 3rd Edition. Addison-Wesley, 1997
[洪琨2003] 洪琨。《如何在 linux 下檢測內(nèi)存泄漏》,IBM developerWorks 中國網(wǎng)站。
吳詠煒,目前在Linux上從事高性能入侵檢測系統(tǒng)的研發(fā)。對于開發(fā)跨平臺、高性能、可重用的C++代碼有著濃厚的興趣。adah@sh163.net可以跟他聯(lián)系。
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
C++操作符重載手冊
理解C++中new背后的行為
newnewnew(transfered) - fleeting_ash的日志 - 網(wǎng)易博...
C++中運算符New的三種使用方式
如何在linux下檢測內(nèi)存泄漏
C++編程實用技巧 #31:千萬不要返回局部對象的引用
更多類似文章 >>
生活服務(wù)
分享 收藏 導長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服