1. 程式人生 > >OO程式設計思想之一---物件生命週期與記憶體模型

OO程式設計思想之一---物件生命週期與記憶體模型

記憶體模型基礎

====================================================

記憶體模型是隨著越來越豐富和複雜的物件生命週期要求的發展而發展起來的。

最初的記憶體模型完全是線性的,靜態的,一個程式執行時所有需要的物件都是在執行前完全準備好了的,執行完了時釋放掉。典型的代表就是Fortran語言。這種語言的執行效能非常高(當然了,沒有任何別的消耗嘛),但是表達能力受到限制(畢竟,要求靜態的確定一切物件和記憶體的繫結關係)。最明顯的一個限制就是沒辦法支援遞迴。這種記憶體模型支援的物件的生命週期跟應用程式的生命週期完全一致。同生共死,天下大同。

Alogal的出現引入了一個強大的概念: lexical scope,記憶體模型也相應的出現了細分概念:棧。棧就是那種先進後出的容器

,它完美的切合了lexical scope。同時,棧上的物件的生命週期也很清晰,進棧是出生,出棧時死亡。棧這個概念是如此的清晰、容易理解,而且如此的強大,導致現代各種語言的記憶體模型中,棧都佔有非常顯著的地位。棧自然地解決了遞迴問題。棧上物件的生命週期的管理完全可以自動化而高效的完成。但是,棧仍然不足以滿足某些物件生命週期的要求。

我先說說這是那種樣子的生命週期。簡單的說,棧對於共享的物件的生命週期無能為力。也就是說,某個物件,在多個平級的或者巢狀的scope之間共享//例:多執行緒中的共享,而棧卻沒辦法解決。非得用棧來滿足這種需求的話,會導致大量的memcpy,導致效能的低下。而且還是模擬的解決這個問題。其實,對於引數和返回值,基於棧架構的記憶體模型和物件就是採用複製和反向複製的方式來完成的。

面對這個挑戰(共享物件生命週期問題),第一個反應就是避免共享,其實就是我前面說的完全副本的方式。這種方式在一定程度上有效,不過,對於大物件是非常不划算的。

C 和Pascal語言來了,這類語言提供了另一個概念叫做堆(其實,堆這個概念並不是它們最先引入的,但是是由它們發揚光大的)。從此內存模型分裂為兩個界限明顯的型別:棧和堆堆就是那種隨機進隨機出的物件的棲息之地。也就是說,這種物件的生命週期不再可以向棧物件那樣自動高效的管理了。有了這樣的基礎設施,共享物件就變得簡單了。建立一個堆物件,然後A使用之,只要不銷燬,B就可以使用之,這樣,該物件就可以由A和B共享了。當然,牽扯到共享,必然會涉及到同步和互斥的問題。也就是A和B究竟怎樣訪問該物件的問題。一般來說,採取的策略是是把併發訪問序列化的技術。由此匯出很多互斥的技術。我們就不在這上面糾纏了。我們關注的是物件本身的生命週期,也就是說。這個可以共享的物件的生、死問題。由誰負責生(建立)似乎大家都沒有什麼抱怨的,由第一個要用該物件的負責,這也不會導致麻煩。原因是:如果你不負責生,那麼該物件就不存在,你也就沒辦法使用了。:)由誰負責死(銷燬)呢。這個看起來也很簡單:最後一個用完了就銷燬。而事實上也確實就這麼簡單。但是這兒有一點麻煩。跟建立不一樣,建立不會被忘記,而銷燬會。忘記銷燬物件對自己沒有傷害,所以,就選擇忘記吧。:)

這樣說吧。現代的語言按照由誰負責堆物件的銷燬問題大致可以分成兩大類,一類叫做有垃圾回收的,一類沒有。有垃圾回收的那一類是由執行環境(執行時)負責銷燬共享物件,沒有垃圾回收的那一類由程式自己負責銷燬共享物件。對程式設計師來說,當然有垃圾回收的語言跟友好了。但問題是垃圾回收需要首先搞定哪一些共享物件不再需要了。這是一個比較困難的問題。而對於沒有垃圾回收的那種語言來說,程式自己邏輯上應該很清楚哪一些物件不再需要了,可以銷燬了。

一般情況下,我們把那些由執行時負責銷燬共享物件的那些堆叫做託管堆,負責銷燬共享物件的執行時的一部分叫做垃圾回收器(GC),也就是說,託管堆就使那種委託給GC管理的堆。
相應的,沒有委託給GC管理的堆就是普通的堆了。普通堆有應用程式自己負責銷燬共享物件。

上面的描述還沒有提到的一個問題是:物件究竟在什麼時候銷燬?由於一個物件不立即銷燬一般不會導致什麼重大的問題,所以銷燬的時機相對來說可以很靈活。當然,肯定是在最後一個使用了以後。但是以後是多久呢?是立即銷燬?還是延遲一段時間?如果是延遲,那麼究竟延遲多長時間?還是更進一步的說延遲不定長的時間?

由於一般情況下,我們認為批量銷燬比一個一個銷燬要快一些,所以一個指導性的方案就是等到垃圾(生命週期應該已經結束了的共享物件)積累到一定的量以後(只要不影響程式的正常執行)批量銷燬。這一般會導致銷燬的不定長延遲。

上面說了:“由於一個物件不立即銷燬一般不會導致什麼重大的問題,所以銷燬的時機相對來說可以很靈活。”。注意這個句子裡面的“一般”這個字眼,這也就是說:在特殊情況下,不立即銷燬會導致重大問題。那種情況是特殊情況呢?就是那種銷燬物件的動作還帶有別的副作用,而這個副作用會影響以後程式的運作行為的情況。這種情況在使用C++的RAII的時候是基本情況。

垃圾回收的基本方法
===========================================

我們把託管堆中的物件以及物件的互相引用關係看作是一個“圖”(數學上定點和邊的集合),應用程式的棧上有一些引用引用到這個圖的某些頂點。從GC的角度來看,應用程式的作用就是不斷地改變圖的連通性,故此把應用程式叫做Mutator。GC就是通過檢視圖的連通性把那些孤立的頂點(就是那種生命週期結束的共享物件)回收的。

那麼,我們怎麼知道圖的連通性的呢?

1、可以通過從根集跟蹤。所謂根集,就是應用程式棧上的引用集合。我們知道,這種跟蹤肯定可以搞定圖的連通性,但是,這要求我們能夠區分什麼是引用,而什麼不是。
2、每次應用程式修改圖的連通性的時候,記錄下來。這樣GC就可以直接銷燬那些孤立的頂點了。
第一種方式是最常用的方式,或者甚至可以這樣說,第二種方式根本沒人用。

不過,第二種方式的某種變形方式用的人卻很多。那就是RefCount。第二種方式把集中存放的連通圖資訊分散的放置到各個共享物件身上,讓他們記住自己被多少個別的物件引用就行了。這樣當它發覺自己被0個物件引用的時候,就自裁。

這個方式看起來非常好,也非常乾淨,但是有兩個缺點。一是效能。每一次引用一個物件,都需要增加這個引用計數,放棄引用的時候得減少之。影響了效能。另一個是對於環形圖的無能為力。
4。類型別的變數的生存期是與其所定義的作用域相關的,也就是說出了“}”,它就立   
刻煙消雲散了。而物件的生存期卻與傳統意義上的作用域沒有任何必然的聯絡,在作  
用域巢狀層次中的任何一個層次中定義(準確地說應該是構造)的物件可以在程式生  
存期的任何時刻被GC回收。  
5。物件被回收必須滿足兩個條件:一是沒有任何指向該物件的強引用(強弱引用這裡  
不詳講);二是GC回收操作被觸發。這兩個條件必須同時滿足,物件才會被回收。    
通常情況下,物件被回收是該物件被利用完以後很久之後的事情了。因此,.net中  
沒有解構函式,只有Finalize函式。dispose()函式的作用只是釋放一些資源(比如  
說開啟的檔案)而以,並不會析構物件。並且,dispose()的使用必須由程式設計師自己   
 控制。  
6。無論如何,物件不能顯式回收。即使你顯式呼叫GC地回收功能,GC常常並不那麼聽    
                  話的去執行回收操作。GC回收操作的觸發是根據一定策略進行的。  
7。應該說,GC回收是安全的,穩定的。但是其開銷也很大,效率不夠高,尤其是佔用  
                  記憶體很多,儘管微軟採用了許多優化措施仍是如此。其開銷也僅僅體現在GC進行垃    
                  圾回收的那一霎那。
 
c#中的物件生命週期
============================================
無論是指型別的變數或是類型別的變數,其儲存單元都是在棧中分配的,唯一不同的是類型別的變數實際上儲存的是該類物件的指標,相當於vc6中的CType*,只是在.net平臺的語言中將指標的概念遮蔽掉了。我們都知道棧的一大特點就是LIFO(後進先出),這恰好與作用域的特點相對應(在作用域的巢狀層次中,越深層次的作用域,其變數的優先順序越高)。因此,對於樓主的問題,再出了“}”後,無論是值型別還是類型別的變數(物件指標)都會被立即釋放(值得注意的是:該指標所指向的託管堆中的物件並未被釋放,正等待GC的回收,這也許才是樓主想問的)。.NET中的棧空間是不歸GC管理的,GC僅管理託管堆。  
          樓主的問題實際上提到了.NET與以前的VS語言的一個根本的不同,主要是由於GC存在。我想就我的理解簡要說明一下:  
          1GC只收集託管堆中的物件。  
          2。所有值型別的變數都在棧中分配,超出作用域後立即釋放棧空間
,這一點與VC6完全  
                一樣。  
          3。區別類型別的變數和類的物件,這是兩個不同的概念。類型別的變數實際上是該類對  
                象的指標變數。如C#中的定義CType   myType;與VC6中的定義CType*   myType;是完全一  
                樣的,只是.net語言將*號隱藏了。與VC6相同,必須用new   關鍵字來構造一個物件,  
                如(C#):CType   myType=new   CType();其實這一條語句有兩次記憶體分配,一次是為類類  
                型變數myType在棧中分配空間(指標型別所佔的空間,對32位系統分配32位,64位  
                系統則分配64位,在同一個系統中,所有指標型別所佔的記憶體空間都是一樣的,而  
                不管該型別的指標所指向的是何種型別的物件),另一次是在託管堆(GC所管理的  
                堆)中構造一個CType型別的物件並將該物件的起始地址賦給變數myType。正因為如  
                此才造成了在同一個作用域中宣告的類型別的變數和該型別的物件的生存期不一樣。  

 
C++中的物件生命週期
===============================
C++中各類物件詳解
如果一個人自稱為程式高手,卻對記憶體一無所知,那麼我可以告訴你,他一定在吹牛。用C或C++寫程式,需要更多地關注記憶體,這不僅僅是因為記憶體的分配是否合理直接影響著程式的效率和效能,更為主要的是,當我們操作記憶體的時候一不小心就會出現問題,而且很多時候,這些問題都是不易發覺的,比如記憶體洩漏,比如懸掛指標。筆者今天在這裡並不是要討論如何避免這些問題,而是想從另外一個角度來認識C++記憶體物件。
我們知道,C++將記憶體劃分為三個邏輯區域:堆、棧和靜態儲存區。既然如此,我稱位於它們之中的物件分別為堆物件,棧物件以及靜態物件。那麼這些不同的記憶體物件有什麼區別了?堆物件和棧物件各有什麼優劣了?如何禁止建立堆物件或棧物件了?這些便是今天的主題。

一.基本概念

先來看看棧。棧,一般用於存放區域性變數或物件,如我們在函式定義中用類似下面語句宣告的物件:

Type stack_object ;

stack_object便是一個棧物件,它的生命期是從定義點開始,當所在函式返回時,生命結束。

另外,幾乎所有的臨時物件都是棧物件。比如,下面的函式定義:

Type fun(Type object) ;

這個函式至少產生兩個臨時物件,首先,引數是按值傳遞的,所以會呼叫拷貝建構函式生成一個臨時物件object_copy1 ,在函式內部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一個棧物件,它在函式返回時被釋放;還有這個函式是值返回的,在函式返回時,如果我們不考慮返回值優化(NRV),那麼也會產生一個臨時物件object_copy2,這個臨時物件會在函式返回後一段時間內被釋放。比如某個函式中有如下程式碼:

Type tt ,result ; //生成兩個棧物件
tt = fun(tt) ; //函式返回時,生成的是一個臨時物件object_copy2

上面的第二個語句的執行情況是這樣的,首先函式fun返回時生成一個臨時物件object_copy2 ,然後再呼叫賦值運算子執行

tt = object_copy2 ; //呼叫賦值運算子

看到了嗎?編譯器在我們毫無知覺的情況下,為我們生成了這麼多臨時物件,而生成這些臨時物件的時間和空間的開銷可能是很大的,所以,你也許明白了,為什麼對於“大”物件最好用const引用傳遞代替按值進行函式引數傳遞了。

接下來,看看堆。堆,又叫自由儲存區,它是在程式執行的過程中動態分配的,所以它最大的特性就是動態性。在C++中,所有堆物件的建立和銷燬都要由程式設計師負責,所以,如果處理不好,就會發生記憶體問題。如果分配了堆物件,卻忘記了釋放,就會產生記憶體洩漏;而如果已釋放了物件,卻沒有將相應的指標置為NULL,該指標就是所謂的“懸掛指標”,再度使用此指標時,就會出現非法訪問,嚴重時就導致程式崩潰。

那麼,C++中是怎樣分配堆物件的?唯一的方法就是用new(當然,用類malloc指令也可獲得C式堆記憶體),只要使用new,就會在堆中分配一塊記憶體,並且返回指向該堆物件的指標。

再來看看靜態儲存區。所有的靜態物件、全域性物件都屬於靜態儲存區分配。關於全域性物件,是在main()函式執行前就分配好了的。其實,在main()函式中的顯示程式碼執行之前,會呼叫一個由編譯器生成的_main()函式,而_main()函式會進行所有全域性物件的的構造及初始化工作。而在main()函式結束之前,會呼叫由編譯器生成的exit函式,來釋放所有的全域性物件。比如下面的程式碼:

void main(void)
{
 … …// 顯式程式碼
}

實際上,被轉化成這樣:

void main(void)
{
 _main(); //隱式程式碼,由編譯器產生,用以構造所有全域性物件
 … … // 顯式程式碼
 … …
 exit() ; // 隱式程式碼,由編譯器產生,用以釋放所有全域性物件
}

所以,知道了這個之後,便可以由此引出一些技巧,如,假設我們要在main()函式執行之前做某些準備工作,那麼我們可以將這些準備工作寫到一個自定義的全域性物件的建構函式中,這樣,在main()函式的顯式程式碼執行之前,這個全域性物件的建構函式會被呼叫,執行預期的動作,這樣就達到了我們的目的。剛才講的是靜態儲存區中的全域性物件,那麼,區域性靜態物件了?區域性靜態物件通常也是在函式中定義的,就像棧物件一樣,只不過,其前面多了個static關鍵字。區域性靜態物件的生命期是從其所在函式第一次被呼叫,更確切地說,是當第一次執行到該靜態物件的宣告程式碼時,產生該靜態區域性物件,直到整個程式結束時,才銷燬該物件。

還有一種靜態物件,那就是它作為class的靜態成員。考慮這種情況時,就牽涉了一些較複雜的問題。

第一個問題是class的靜態成員物件的生命期,class的靜態成員物件隨著第一個class object的產生而產生,在整個程式結束時消亡。也就是有這樣的情況存在,在程式中我們定義了一個class,該類中有一個靜態物件作為成員,但是在程式執行過程中,如果我們沒有建立任何一個該class object,那麼也就不會產生該class所包含的那個靜態物件。還有,如果建立了多個class object,那麼所有這些object都共享那個靜態物件成員。

第二個問題是,當出現下列情況時:

class Base
{
 public:
  static Type s_object ;
}
class Derived1 : public Base / / 公共繼承
{
 … …// other data
}
class Derived2 : public Base / / 公共繼承
{
 … …// other data
}

Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ;

請注意上面標為黑體的三條語句,它們所訪問的s_object是同一個物件嗎?答案是肯定的,它們的確是指向同一個物件,這聽起來不像是真的,是嗎?但這是事實,你可以自己寫段簡單的程式碼驗證一下。我要做的是來解釋為什麼會這樣?我們知道,當一個類比如Derived1,從另一個類比如Base繼承時,那麼,可以看作一個Derived1物件中含有一個Base型的物件,這就是一個subobject。一個Derived1物件的大致記憶體佈局如下:
  
讓我們想想,當我們將一個Derived1型的物件傳給一個接受非引用Base型引數的函式時會發生切割,那麼是怎麼切割的呢?相信現在你已經知道了,那就是僅僅取出了Derived1型的物件中的subobject,而忽略了所有Derived1自定義的其它資料成員,然後將這個subobject傳遞給函式(實際上,函式中使用的是這個subobject的拷貝)。

所有繼承Base類的派生類的物件都含有一個Base型的subobject(這是能用Base型指標指向一個Derived1物件的關鍵所在,自然也是多型的關鍵了),而所有的subobject和所有Base型的物件都共用同一個s_object物件,自然,從Base類派生的整個繼承體系中的類的例項都會共用同一個s_object物件了。上面提到的example、example1、example2的物件佈局如下圖所示:

二.三種記憶體物件的比較

棧物件的優勢是在適當的時候自動生成,又在適當的時候自動銷燬,不需要程式設計師操心;而且棧物件的建立速度一般較堆物件快,因為分配堆物件時,會呼叫 operator new操作,operator new會採用某種記憶體空間搜尋演算法,而該搜尋過程可能是很費時間的,產生棧物件則沒有這麼麻煩,它僅僅需要移動棧頂指標就可以了。但是要注意的是,通常棧空間容量比較小,一般是1MB~2MB,所以體積比較大的物件不適合在棧中分配。特別要注意遞迴函式中最好不要使用棧物件,因為隨著遞迴呼叫深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢位,這樣就會產生執行時錯誤。

堆物件,其產生時刻和銷燬時刻都要程式設計師精確定義,也就是說,程式設計師對堆物件的生命具有完全的控制權。我們常常需要這樣的物件,比如,我們需要建立一個物件,能夠被多個函式所訪問,但是又不想使其成為全域性的,那麼這個時候建立一個堆物件無疑是良好的選擇,然後在各個函式之間傳遞這個堆物件的指標,便可以實現對該物件的共享。另外,相比於棧空間,堆的容量要大得多。實際上,當實體記憶體不夠時,如果這時還需要生成新的堆物件,通常不會產生執行時錯誤,而是系統會使用虛擬記憶體來擴充套件實際的實體記憶體。
接下來看看static物件。

首先是全域性物件。全域性物件為類間通訊和函式間通訊提供了一種最簡單的方式,雖然這種方式並不優雅。一般而言,在完全的面嚮物件語言中,是不存在全域性物件的,比如C#,因為全域性物件意味著不安全和高耦合,在程式中過多地使用全域性物件將大大降低程式的健壯性、穩定性、可維護性和可複用性。C++也完全可以剔除全域性物件,但是最終沒有,我想原因之一是為了相容C。

其次是類的靜態成員,上面已經提到,基類及其派生類的所有物件都共享這個靜態成員物件,所以當需要在這些class之間或這些class objects之間進行資料共享或通訊時,這樣的靜態成員無疑是很好的選擇。

接著是靜態區域性物件,主要可用於儲存該物件所在函式被屢次呼叫期間的中間狀態,其中一個最顯著的例子就是遞迴函式,我們都知道遞迴函式是自己呼叫自己的函式,如果在遞迴函式中定義一個nonstatic區域性物件,那麼當遞迴次數相當大時,所產生的開銷也是巨大的。這是因為nonstatic區域性物件是棧物件,每遞迴呼叫一次,就會產生一個這樣的物件,每返回一次,就會釋放這個物件,而且,這樣的物件只侷限於當前呼叫層,對於更深入的巢狀層和更淺露的外層,都是不可見的。每個層都有自己的區域性物件和引數。

在遞迴函式設計中,可以使用static物件替代nonstatic區域性物件(即棧物件),這不僅可以減少每次遞迴呼叫和返回時產生和釋放nonstatic物件的開銷,而且static物件還可以儲存遞迴呼叫的中間狀態,並且可為各個呼叫層所訪問。

三.使用棧物件的意外收穫

前面已經介紹到,棧物件是在適當的時候建立,然後在適當的時候自動釋放的,也就是棧物件有自動管理功能。那麼棧物件會在什麼會自動釋放了?第一,在其生命期結束的時候;第二,在其所在的函式發生異常的時候。你也許說,這些都很正常啊,沒什麼大不了的。是的,沒什麼大不了的。但是隻要我們再深入一點點,也許就有意外的收穫了。

棧物件,自動釋放時,會呼叫它自己的解構函式。如果我們在棧物件中封裝資源,而且在棧物件的解構函式中執行釋放資源的動作,那麼就會使資源洩漏的概率大大降低,因為棧物件可以自動的釋放資源,即使在所在函式發生異常的時候。實際的過程是這樣的:函式丟擲異常時,會發生所謂的 stack_unwinding(堆疊回滾),即堆疊會展開,由於是棧物件,自然存在於棧中,所以在堆疊回滾的過程中,棧物件的解構函式會被執行,從而釋放其所封裝的資源。除非,除非在解構函式執行的過程中再次丟擲異常――而這種可能性是很小的,所以用棧物件封裝資源是比較安全的。基於此認識,我們就可以建立一個自己的控制代碼或代理來封裝資源了。智慧指標(auto_ptr)中就使用了這種技術。在有這種需要的時候,我們就希望我們的資源封裝類只能在棧中建立,也就是要限制在堆中建立該資源封裝類的例項。

四.禁止產生堆物件

上面已經提到,你決定禁止產生某種型別的堆物件,這時你可以自己建立一個資源封裝類,該類物件只能在棧中產生,這樣就能在異常的情況下自動釋放封裝的資源。

那麼怎樣禁止產生堆物件了?我們已經知道,產生堆物件的唯一方法是使用new操作,如果我們禁止使用new不就行了麼。再進一步,new操作執行時會呼叫 operator new,而operator new是可以過載的。方法有了,就是使new operator 為private,為了對稱,最好將operator delete也過載為private。現在,你也許又有疑問了,難道建立棧物件不需要呼叫new嗎?是的,不需要,因為建立棧物件不需要搜尋記憶體,而是直接調整堆疊指標,將物件壓棧,而operator new的主要任務是搜尋合適的堆記憶體,為堆物件分配空間,這在上面已經提到過了。好,讓我們看看下面的示例程式碼:

#include <stdlib.h> //需要用到C式記憶體分配函式
class Resource ; //代表需要被封裝的資源類
class NoHashObject
{
 private:
  Resource* ptr ;//指向被封裝的資源
  ... ... //其它資料成員
  void* operator new(size_t size) //非嚴格實現,僅作示意之用
  {
   return malloc(size) ;
  }
  void operator delete(void* pp) //非嚴格實現,僅作示意之用
  {
   free(pp) ;
  }
 public:
  NoHashObject()
  {
   //此處可以獲得需要封裝的資源,並讓ptr指標指向該資源
   ptr = new Resource() ;
  }
  ~NoHashObject()
  {
   delete ptr ; //釋放封裝的資源
  }
};

NoHashObject現在就是一個禁止堆物件的類了,如果你寫下如下程式碼:

NoHashObject* fp = new NoHashObject() ; //編譯期錯誤!
delete fp ;

上面程式碼會產生編譯期錯誤。好了,現在你已經知道了如何設計一個禁止堆物件的類了,你也許和我一樣有這樣的疑問,難道在類NoHashObject的定義不能改變的情況下,就一定不能產生該型別的堆物件了嗎?不,還是有辦法的,我稱之為“暴力破解法”。C++是如此地強大,強大到你可以用它做你想做的任何事情。這裡主要用到的是技巧是指標型別的強制轉換。

void main(void)
{
 char* temp = new char[sizeof(NoHashObject)] ;

 //強制型別轉換,現在ptr是一個指向NoHashObject物件的指標
 NoHashObject* obj_ptr = (NoHashObject*)temp ;

 temp = NULL ; //防止通過temp指標修改NoHashObject物件

 //再一次強制型別轉換,讓rp指標指向堆中NoHashObject物件的ptr成員
 Resource* rp = (Resource*)obj_ptr ;

 //初始化obj_ptr指向的NoHashObject物件的ptr成員
 rp = new Resource() ;
 //現在可以通過使用obj_ptr指標使用堆中的NoHashObject物件成員了
 ... ...

 delete rp ;//釋放資源
 temp = (char*)obj_ptr ;
 obj_ptr = NULL ;//防止懸掛指標產生
 delete [] temp ;//釋放NoHashObject物件所佔的堆空間。
}

上面的實現是麻煩的,而且這種實現方式幾乎不會在實踐中使用,但是我還是寫出來路,因為理解它,對於我們理解C++記憶體物件是有好處的。對於上面的這麼多強制型別轉換,其最根本的是什麼了?我們可以這樣理解:

某塊記憶體中的資料是不變的,而型別就是我們戴上的眼鏡,當我們戴上一種眼鏡後,我們就會用對應的型別來解釋記憶體中的資料,這樣不同的解釋就得到了不同的資訊。

所謂強制型別轉換實際上就是換上另一副眼鏡後再來看同樣的那塊記憶體資料。

另外要提醒的是,不同的編譯器對物件的成員資料的佈局安排可能是不一樣的,比如,大多數編譯器將NoHashObject的ptr指標成員安排在物件空間的頭4個位元組,這樣才會保證下面這條語句的轉換動作像我們預期的那樣執行:

Resource* rp = (Resource*)obj_ptr ;

但是,並不一定所有的編譯器都是如此。

既然我們可以禁止產生某種型別的堆物件,那麼可以設計一個類,使之不能產生棧物件嗎?當然可以。

五.禁止產生棧物件

前面已經提到了,建立棧物件時會移動棧頂指標以“挪出”適當大小的空間,然後在這個空間上直接呼叫對應的建構函式以形成一個棧物件,而當函式返回時,會呼叫其解構函式釋放這個物件,然後再調整棧頂指標收回那塊棧記憶體。在這個過程中是不需要operator new/delete操作的,所以將operator new/delete設定為private不能達到目的。當然從上面的敘述中,你也許已經想到了:將建構函式或解構函式設為私有的,這樣系統就不能呼叫構造/析構函數了,當然就不能在棧中生成物件了。

這樣的確可以,而且我也打算採用這種方案。但是在此之前,有一點需要考慮清楚,那就是,如果我們將建構函式設定為私有,那麼我們也就不能用new來直接產生堆物件了,因為new在為物件分配空間後也會呼叫它的建構函式啊。所以,我打算只將解構函式設定為private。再進一步,將解構函式設為private除了會限制棧物件生成外,還有其它影響嗎?是的,這還會限制繼承。

如果一個類不打算作為基類,通常採用的方案就是將其解構函式宣告為private。

為了限制棧物件,卻不限制繼承,我們可以將解構函式宣告為protected,這樣就兩全其美了。如下程式碼所示:

class NoStackObject
{
 protected:
  ~NoStackObject() { }
 public:
  void destroy()
  {
   delete this ;//呼叫保護解構函式
  }
};

接著,可以像這樣使用NoStackObject類:

NoStackObject* hash_ptr = new NoStackObject() ;
... ... //對hash_ptr指向的物件進行操作
hash_ptr->destroy() ;

呵呵,是不是覺得有點怪怪的,我們用new建立一個物件,卻不是用delete去刪除它,而是要用destroy方法。很顯然,使用者是不習慣這種怪異的使用方式的。所以,我決定將建構函式也設為private或protected。這又回到了上面曾試圖避免的問題,即不用new,那麼該用什麼方式來生成一個物件了?我們可以用間接的辦法完成,即讓這個類提供一個static成員函式專門用於產生該型別的堆物件。(設計模式中的singleton模式就可以用這種方式實現。)讓我們來看看:

class NoStackObject
{
 protected:
  NoStackObject() { }
  ~NoStackObject() { }
 public:
  static NoStackObject* creatInstance()
  {
   return new NoStackObject() ;//呼叫保護的建構函式
  }
  void destroy()
  {
   delete this ;//呼叫保護的解構函式
  }
};

現在可以這樣使用NoStackObject類了:

NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
... ... //對hash_ptr指向的物件進行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用懸掛指標

現在感覺是不是好多了,生成物件和釋放物件的操作一致了。