C++中指針的使用在為語言提供極大的靈活性的同時,也使得記憶體管理變得極其複雜。如何利用好指針,并在工程實踐中組織好高效的記憶體管理政策,是一個C++項目按時傳遞、并取得成功的關鍵。
本文将從理論和實踐上分析,如何通過良好的類的設計政策,來實踐一個可在工程中使用的有效的記憶體管理方案。
本文的組織結構如下:
1:首先,以面向對象的觀點,從總體架構上分析,一個有效的記憶體管理政策應該達到一個怎樣的效果。
2:接着,從類層次出發,讨論針對類的設計的準則和政策。
3:然後,再深入細節,闡述實踐中可以使用的設計方法和需要注意的一些設計細節。
4:最後,再一次回到宏觀的架構上,談談在一個實際的項目中,如何根據項目的實際情況,權衡得失,并考慮到運用自定義的記憶體管理自身的利弊,進而來選擇真正合适具體項目的實作方案。
1:怎樣才算是一個有效的記憶體管理方案?
C++中一個核心的概念就是面向對象。面向對象的思想告訴我們:一個項目的基本組成元素是類的對象。而項目功能的實作是通過對象之間的發送和接收消息來實作的。是以,在高層的設計中,一個理想的main函數以及其他大部分的函數中,我們應該隻會看到類對象和消息(或稱 接口、函數)的存在,而并沒有指針的概念的存在。即:将指針的使用隐藏在類的實作的背後!
指針的使用是為了提高程式的靈活性,而這一靈活性的實作具體就展現在動态記憶體的配置設定上,而一說到動态記憶體配置設定問題,則記憶體管理的問題也就接踵而來。由此可以,指針和記憶體管理這兩個屬于低層設計的概念是與生俱來的被聯系在一起的。要實作合理的記憶體管理的過程,也就是實作合理的指針的封裝和使用的過程。
2:用類來封裝指針,隐藏記憶體管理的細節
類的設計的重要職能就是:提供高層抽象,隐藏實作細節。
客戶在使用一個類的時候,關注的是它所提供的功能,而對于類的記憶體布局、實作細節等,一概不需要知道。客戶甚至不應該知道類的對象是怎樣建立的(是堆上還是棧上)。
注意:我們這裡所說的客戶主要指的是類的調用方程式代碼。為了實作這一職能目标,類的設計應該滿足以下準則:
(1)RAII: Resource Acquisition IsInitialization
(2)類關注的是其成員變量的記憶體配置設定和釋放,而無需關心類自身的記憶體配置設定和釋放。
這是一個遞歸的定義:類的成員變量可以是一個類對象,而類本身所産生的對象也可以作為其他類的成員變量。那麼,這一遞歸在啥時應該終止呢?
答案是:我們所要實作的功能在哪個抽象層上,那麼相應的類就定義到該抽象層上為止。即:在該抽象層上的類已經包含了我們要實作的功能的完整的描述,是以可以直接使用,而不需要作為其他類的一個成員變量。
(3)用棧對象來封裝堆對象
智能指針是這一準則的一個實作。
這裡給出一個标準,可以用來判斷你是否很好地使用了上述三個準則:new/delete,malloc/free隻會出現在類内部的構造/析構函數以及棧對象的初始化中,而在程式實作的業務邏輯中不會出現顯式的動态資源配置設定。
當然,這一标準也不應該絕對化,但我們應該盡量向之看齊。
3:可用的實作方法和技術
(1) 重載operator new/delete
(2) 在constructor/destructor中完成資源的配置設定和釋放,并根據需要重載/禁止 複制/指派 構造/析構 函數。
(3) 智能指針(引用計數、所有權的轉移)
(4) 從嚴格意義上來說,每個類都有屬于為自己而設計的resource allocator,以針對類自身的特點,給出相應的資源配置設定方案。
如:針對大量的小對象,可考慮使用資源池。
4:具體問題具體分析
(1) 項目有大有小
如果項目很小,或者類比較少,又或者動态配置設定資源的情況不多,則引入複雜的類的記憶體管理可能不是必需的。而如果項目較大,則應該給出完整的記憶體管理方案。
(2) 使用第三方庫的利與弊
Boost中的shared_ptr,scoped_ptr提供了可用的智能指針,可以為己所用,但也要考慮項目的擴充性以及後期維護的成本。引入第三方庫,也同時意味着引入了外來的不确定性,這會影響項目的進度預算的可信度。
(3) 實作記憶體管理的代碼自身所引入的錯誤
為了實作完整的記憶體管理,這勢必增加代碼量。這裡要記住的是:增加的工作量永遠都是值得的,但是前提是 一定要確定記憶體管理代碼本身的正确性!