/****************************************************************/
/* 學習是合作和分享式的!
/* Author:Atlas Email:[email protected]
/****************************************************************/
上節內容回顧:傳送門
1.C++內存管理
1.1c語言和C++內存分配
1.2區分堆、棧、靜態存儲區
1.3控制C++的內存分配
在C++中一種常見的問題是對內存的分配,重點是new和delete的使用不當而失控。一般來說,C++對內存的管理非常的容易和安全,當一個對象被消除時,它的析構函數能夠安全的釋放所分配的內存。所以頻繁的使用new和delete動態分配會出現一些問題和堆破碎的風險。
所以,當你必須要使用new 和delete時,你不得不控制C++中的內存分配。你需要用一個全局的new 和delete來代替系統的內存分配符,並且一個類一個類的重載new 和delete。
一個防止堆破碎的通用方法是從不同固定大小的內存池中分配不同類型的對象。對每個類重載new和delete就提供了這樣的控制。
小問:為什麼需要重載new和delete?
雖然C++標准庫已經為我們提供了new與delete操作符的標准實現,但是由於缺乏對具體對象的具體分析,系統默認提供的分配器在時間和空間兩方面都存在著一些問題:分配器速度較慢,而且在分配小型對象時空間浪費比較嚴重,特別是在一些對效率或內存有較大限制的特殊應用中。比如說在嵌入式的系統中,由於內存限制,頻繁地進行不定大小的內存動態分配很可能會引起嚴重問題,甚至出現堆破碎的風險;再比如在游戲設計中,效率絕對是一個必須要考慮的問題,而標准new與delete操作符的實現卻存在著天生的效率缺陷。此時,我們可以求助於new與delete操作符的重載,它們給程序帶來更靈活的內存分配控制。除了改善效率,重載new與delete還可能存在以下幾點原因:
a)檢測代碼中的內存錯誤。
b)性能優化
b)獲得內存使用的統計數據。
在《Effective c++》一書中也有講解為什麼需要重載,似乎重載是必須的?對於何種情況下需要重載new和delete的問題,以及如何重載的問題,需要繼續研究學習。
詳見文章-建議33:小心翼翼地重載operator new/ operator delete
(1)重載全局的new和delete
以下代碼為《c++內存管理技術內幕》中是的,只限於簡單原理學習
1: void * operator new(size_t size) 2: { 3: void *p = malloc(size); 4: return (p); 5: } 6: void operator delete(void *p) 7: { 8: free(p); 9: }
這段代碼可以代替默認的操作符來滿足內存分配的請求。出於解釋C++的目的,我們也可以直接調用malloc()和free()。
也可以對單個類的new 和 delete 操作符重載。這是你能靈活的控制對象的內存分配。
1: class TestClass { 2: public: 3: void * operator new(size_t size); 4: void operator delete(void *p); 5: // .. other members here ... 6: }; 7: void *TestClass::operator new(size_t size) 8: { 9: void *p = malloc(size); // Replace this with alternative allocator 10: return (p); 11: } 12: void TestClass::operator delete(void *p) 13: { 14: free(p); // Replace this with alternative de-allocator 15: }
所有TestClass 對象的內存分配都采用這段代碼。更進一步,任何從TestClass 繼承的類也都采用這一方式,除非它自己也重載了new 和 delete 操作符。通過重載new 和 delete 操作符的方法,你可以自由地采用不同的分配策略,從不同的內存池中分配不同的類對象。
(2)為單個類重載new[ ]和delete[ ]
必須小心對象數組的分配。你可能希望調用到被你重載過的new 和 delete 操作符,但並不如此。內存的請求被定向到全局的new[ ]和delete[ ] 操作符,而這些內存來自於系統堆。
C++將對象數組的內存分配作為一個單獨的操作,而不同於單個對象的內存分配。為了改變這種方式,你同樣需要重載new[ ] 和 delete[ ]操作符。
1: class TestClass { 2: public: 3: void * operator new[ ](size_t size); 4: void operator delete[ ](void *p); 5: // .. other members here .. 6: }; 7: void *TestClass::operator new[ ](size_t size) 8: { 9: void *p = malloc(size); 10: return (p); 11: } 12: void TestClass::operator delete[ ](void *p) 13: { 14: free(p); 15: } 16: int main(void) 17: { 18: TestClass *p = new TestClass[10]; 19: // ... etc ... 20: delete[ ] p; 21: }
但是注意:對於多數C++的實現,new[]操作符中的個數參數是數組的大小加上額外的存儲對象數目的一些字節。在你的內存分配機制重要考慮的這一點。你應該盡量避免分配對象數組,從而使你的內存分配策略簡單。
另:對於重載new和delete或者new[ ] 和delete[ ],需要考慮諸多事宜,比如錯誤處理機制,繼承、多態等問題。限於篇幅,將在以後的文章中詳細講解,在此買一個伏筆。
(可以參考一篇文章new、delete(new[]、delete[])操作符的重載)。
1.4 內存管理的基本要求
如果只考慮分配和釋放,內存管理基本要求是“不重不漏”:既不重復 delete,也不漏掉 delete。也就說我們常說的 new/delete 要配對,“配對”不僅是個數相等,還隱含了 new 和 delete 的調用本身要匹配,不要“東家借的東西西家還”。例如:
用系統默認的 malloc() 分配的內存要交給系統默認的 free() 去釋放;
用系統默認的 new 表達式創建的對象要交給系統默認的 delete 表達式去析構並釋放;
用系統默認的 new[] 表達式創建的對象要交給系統默認的 delete[] 表達式去析構並釋放;
用系統默認的 ::operator new() 分配的的內存要交給系統默認的 ::operator delete() 去釋放;
用 placement new 創建的對象要用 placement delete (為了表述方便,姑且這麼說吧)去析構(其實就是直接調用析構函數);
從某個內存池 A 分配的內存要還給這個內存池。
如果定制 new/delete,那麼要按規矩來。見 Effective C++ 相關條款。做到以上是每個 C++ 開發人員的基本功。
1.5常見的內存錯誤及其對策
發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時用戶怒氣沖沖地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。
常見的內存錯誤如下:
(1)內存分配未成功,卻使用了它。
編程新手常犯這種錯誤,因為他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否為NULL。如果指針p是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行檢查。如果是用malloc或new來申請內存,應該用if(p==NULL) 或if(p!=NULL)進行防錯處理。
(2) 內存分配雖然成功,但是尚未初始化就引用它。
犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內存的缺省初值全為零,導致引用初值錯誤(例如數組)。 內存的缺省初值究竟是什麼並沒有統一的標准,盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
(3)內存分配成功並且已經初始化,但操作越過了內存的邊界。
例如在使用數組時經常發生下標“多1”或者“少1”的操作。特別是在for循環語句中,循環次數很容易搞錯,導致數組操作越界。
(4)忘記了釋放內存,造成內存洩露。
含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。動態內存的申請與釋放必須配對,程序中malloc與free的使用次數一定要相同,否則肯定有錯誤(new/delete同理)。
(5)釋放了內存卻繼續使用它。
有三種情況:
(a)程序中的對象調用關系過於復雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。
(b)函數的return語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀。
(c)使用free或delete釋放了內存後,沒有將指針設置為NULL。導致產生“野指針”。
常見的內存錯誤對策如下:
【規則1】用malloc或new申請內存之後,應該立即檢查指針值是否為NULL。防止使用指針值為NULL的內存。
【規則2】不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。
【規則3】避免數組或指針的下標越界,特別要當心發生“多1”或者“少1”操作。
【規則4】動態內存的申請與釋放必須配對,防止內存洩漏。
【規則5】用free或delete釋放了內存之後,立即將指針設置為NULL,防止產生“野指針”。
--------------------------------------------------------------------------------
這部分學習筆記,其中有很多可以深究的知識,比如new和delete的重載、內存錯誤情況及處理機制等等,學無止境。