關於C++內存管理這話題,永遠都不過期。在我剛出道的時候,就已經在考慮怎麼檢測內存泄漏(http://www.javashuo.com/article/p-kdnxlkew-z.html)。想用一份簡單的代碼,而且不太影響執行效率去實現內存泄漏檢測,是不太現實的。當時以爲重寫new和delete是沒有太大價值的,不事後來在本身的項目中仍是重寫了,加了個計數。在程序退出時檢測下計數new的次數和delete次數是否對得上,對不上就是有問題了,再用valgrind之類的工具去檢測。這種排除不了全部狀況,但確實也解決了一些問題。畢竟每次寫新功能時發現問題立馬去解決,比你寫了成千上萬個功能,上線後出問題再查找容易得多。html
在windows下則有另外一種方案, C Run-time Library (CRT) debug,_CrtDumpMemoryLeaks()函數,這也僅僅是發現泄漏,定位還得用另外一個工具visual leak detector。最近在解決公司程序內存泄漏過程當中,發現其實並無內存泄漏,而是程序是在atexit裏調用_CrtDumpMemoryLeaks()函數的,而static變量申請的內存,可能要在atexit回調以後釋放。由此,我突然想到我之前重寫new和delete,有些地方寫得並不對,在這裏從新整理一下。ios
new、delete並非一個函數,它在編譯的時候會被解析成三個步驟:1.調用operator new分配內存;2.調用構造函數;3.把指針轉換成對應的類型返回。可以重寫的,是operator new函數。git
#include <cstdlib> #include <iostream> int g_counter = 0; void *operator new(size_t size) { ++g_counter; std::cout << "new mem:" << g_counter << std::endl; return ::malloc(size); } void operator delete(void* ptr) { --g_counter; std::cout << "delete mem:" << g_counter << std::endl; ::free(ptr); } void on_exit() { std::cout << "exit,mem counter = " << g_counter << std::endl; } int main() { atexit(on_exit); char *ptr = new char[8]; return 0; }
$ g++ main.cpp$ ./a.out new mem:1 exit,mem counter = 1
上面簡單地重寫了operator new和operator delete,在程序退出時能夠檢測到還有一次內存沒釋放掉。但上面的代碼存在不少問題。github
1. 儘可能重寫全部函數web
C++的operator new和operator delete函數一般比你想像中的多。並且不一樣的版本會帶來不一樣的函數,1七、20版本都相應的增長了一些函數,參考https://en.cppreference.com/w/cpp/memory/new/operator_new。 若是你沒有重寫完,雖然能編譯經過,但可能並非你想要的結果。好比上面的代碼,new char[8]本應該調用operator new[]函數的,因爲沒有重寫operator new[],默認調用了libstdcxx中的operator new[],默認函數又調用了operator new。雖然這不必定有什麼問題,但在某些項目中,對內存分配作了特殊處理,或者一些特殊操做(好比一個內存池重寫了operator new[]但沒重寫operator delete[],而他們的內存是回收到不一樣地方),這就會出問題。windows
2. 利用atexit統計內存並不許確安全
atexit是在程序退出時調用,對絕大多數變量來講都是OK的,但對static和global變量則不必定了。根據C++標準:https://isocpp.org/files/papers/N3690.pdf 3.6.3 Termination,atexit註冊以前就已經建立的變量,則在atexit以後釋放,這意味着你的static和global變量若是new了內存必須在atexit以後建立。但這又引出C++的另外一個問題:static initialization order fiasco。固然咱們有不少方法去處理它,好比把全部static和global放到一個cpp文件裏,或者在程序退出時手動釋放new的內存。另外,gcc連接的時候,放在最後的object文件裏的global變量會優先初始化,或者用gcc的__attribute__ ((init_priority (N)))屬性來指定初始化優先級,但這不是標準,不過這畢竟是值得注意的地方。多線程
3. 線程安全函數
上面的代碼沒有加鎖,因此是不能用在多線程中的。但如今有幾個程序不用多線程的,因此仍是得把鎖加上,加鎖的代碼很簡單。工具
static pthread_mutex_t *counter_mutex() { static pthread_mutex_t _mutex; assert( 0 == pthread_mutex_init( &_mutex,NULL ) ); return &_mutex; } static pthread_mutex_t *_mem_mutex_ = counter_mutex(); int g_counter = 0; void *operator new(size_t size) { assert( _mem_mutex_ ); pthread_mutex_lock( _mem_mutex_ ); ++g_counter; pthread_mutex_unlock( _mem_mutex_ ); std::cout << "new mem:" << g_counter << std::endl; return ::malloc(size); }
4. 線程安全帶來初始化問題
在上面說atexit統計內存不許確的時候提到static initialization order fiasco的問題,在這裏變得更嚴重了。由於線程安全是用一個static pthread_mutex_t指針來實現的,那麼在其餘global變量建立時若是調用了new,那麼它多是沒有被初始化的。固然若是你已按上面的方法解決了,那就不會有這個問題了。或者,根據C++標準,Static initialization初始化必須在全部Dynamic initialization以前,咱們能夠這樣寫:
/* Static initialization */ static pthread_mutex_t *_mem_mutex_ = NULL; class global_static { public: global_static() { assert( !_mem_mutex_ ); _mem_mutex_ = counter_mutex(); } ~global_static() {_mem_mutex_ = NULL;} }; /* Dynamic initialization */ const static global_static gs;
這樣雖然不能解決問題,可是因爲咱們在new裏校驗了_mem_mutex_是否爲NULL,至少能發現問題。
既然是C++,那麼還能夠Construct On First Use Idiom:在使用_mem_mutex_時去檢測是否已初始化,未初始化就初始化。而不是像上面那樣全局一次初始化,之後都不用檢測。
5. 可否統計到STL、BOOST、so、.a等外部代碼中的new、delete是否會被重寫
STL和BOOST這種不少時候是是模板,也就是源碼,和你項目中的代碼同樣,固然也會被重寫。對於so動態連接庫,他和程序是分離的。當你的程序加載這個so文件時,它會優先在你的程序裏查找他須要的符號,若是找到了,就會優先使用。這和LD_PRELOAD的機制是同樣的,所以也是會被重寫的。而.a這種靜態連接庫,在gcc連接時會按你傳入的庫順序查找符號,通常來講你項目中的符號都是優先於libgcc這種標準庫的,所以也是會被重寫的。
要明白這些,要懂得gcc是如何編譯、連接一個程序的,尤爲是對符號的管理。https://akkadia.org/drepper/dsohowto.pdf
6. 能夠用nm來判斷是否重寫
xzc@xzc-HP-ProBook-4446s:~/Documents/code/test$ nm -C a.out | grep new 0000000000400f60 T test_static_new() U operator new[](unsigned long)@@GLIBCXX_3.4 0000000000400d34 T operator new(unsigned long) 0000000000401120 r operator new(unsigned long)::__PRETTY_FUNCTION__
T表示text,說明你已經重寫了。U表示undefine,表示沒有重寫,程序運行時,要去庫裏查找這個符號。
大部分人重寫operator new和operator delete的初衷,無非就是檢測內存泄漏,或者實現本身的內存管理。對於內存泄漏,經過重寫operator new來實現的,能夠看http://wyw.dcweb.cn/leakage.htm這裏,如今還在維護的項目是https://github.com/adah1972/nvwa,我沒用過,但看下邏輯應該仍是不錯的。而對於在代碼中重寫operator new來實現內存管理,我倒沒見過。畢竟想寫一個通用的內存管理不容易,寫出來也是一個庫了,好比jemalloc這種。