天天看點

資源管理

  所謂資源就是,一旦用了它,将來必須還給系統。C++程式中最常使用的資源就好似動态配置設定記憶體(如果你new了,卻忘了delete,會導緻記憶體洩露),但記憶體隻是你必須管理的衆多資源之一。其它常見的有檔案描述符(file descriptors)、互斥器(mutex)、圖形界面中的字形和畫刷。資料庫連接配接以及網絡sockets。當你不使用它們時,記得還給系統。

  當你考慮到異常、函數内多重回傳路徑、程式維護員改動軟體卻沒能充分了解随之而來的沖擊,那麼資源管理就顯得複雜的多。

  工廠函數:會傳回一個base class指針,指向新生成之derived class對象。

  解決方法:把資源放進對象内,我們便可依賴C++的“析構函數自動調用機制”確定資源被釋放。

  許多資源被動态配置設定于堆内而後被用于單一區塊或函數内。它們應該在控制流離開那個區塊或函數時被釋放。标準程式庫提供的auto_ptr正是針對這種形勢而設計的特制産品。auto_ptr是個“類指針對象”,也就是所謂的“智能指針”,其析構函數自動對其所指對象調用delete。

  許多資源被動态配置設定于heap内而後被用于單一區塊或函數内,他們應該在控制流離開那個區塊或函數時被釋放。

“以對象管理資源”的兩個關鍵想法:

獲得資源後立刻放進管理對象内(如auto_ptr)。每一筆資源都在獲得的同時立刻被放進管理對象中。“資源取得時機便是初始化時機”(Resource Acquisition Is Initialization;RAII)。

管理對象運用析構函數確定資源被釋放。不論控制流如何離開區塊,即一旦對象被銷毀,其析構函數被自動調用來釋放資源。

  由于auto_ptr被銷毀時會自動删除它所指之物,是以不能讓多個auto_ptr同時指向同一對象。是以auto_ptr若通過copy構造函數或copy assignment操作符複制它們,它們會變成NULL,而複制所得的指針将取得資源的唯一擁有權!

  受auto_ptr管理的資源必須絕對沒有一個以上的auto_ptr同時指向它。

  auto_ptr的替代方案是“引用計數型智能指針”(reference-counting smart pointer;SCSP)、它可以持續跟蹤共有多少對象指向某筆資源,并在無人指向它時自動删除該資源。但是SCSPs無法打破環狀引用,如兩個已經沒有彼此使用的對象彼此互指,好像還在“被使用”狀态。

  TR1的tr1::shared_ptr就是一個"引用計數型智能指針"。

  auto_ptr和tr1::shared_ptr都在其析構函數内做delete而不是delete[],也就意味着在動态配置設定而得的數組身上使用auto_ptr或tr1::shared_ptr是個潛在危險,資源得不到釋放。也許boost::scoped_array和boost::shared_array能提供幫助。還有,vector和string幾乎總是可以取代動态配置設定而得的數組。

請記住:

為防止資源洩漏,請使用RAII對象,它們在構造函數中獲得資源并在析構函數中釋放資源。

兩個常被使用的RAII類分别是auto_ptr和tr1::shared_ptr。後者通常是較佳選擇,因為其拷貝行為比較直覺。若選擇auto_ptr,複制動作會使他(被複制物)指向NULL。

  我們在條款13中讨論的資源表現在堆上申請的資源,而有些資源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我們需要建立自己的資源管理類。

  我們建立的資源管理類可能會是這樣:

  “當一個RAII對象被複制,會發生什麼事?”大多數時候你會選擇一下兩種可能:

禁止複制。如果複制動作對RAII類并不合理,你便應該禁止之。禁止類的copying函數參見條款6。

對底層資源使用”引用計數法“。有時候我們又希望保有資源,直到它的最後一個使用者被銷毀。這種情況下複制RAII對象時,應該将資源的”被引用計數“遞增。tr1::shared_ptr便是如此。

  通常隻要内含一個tr1::shared_ptr成員變量,RAII類便可實作”引用計數“行為。

  由于tr1::shared_ptr允許我們指定”引用計數“為0時被調用的所謂”删除器“,那是一個函數或函數對象,當是”當引用計數”為0時被調用(此基能并不存在auto_ptr,他總是将其指針删除)。删除器對shared_ptr構造函數而言是可有可無的第二參數,是以代碼看起來如上。

  本例中,并沒說明析構函數,因為沒有必要。編譯器為我們生成的析構函數會自動調用其non-static成員變量(mutexPtr)的析構函數。而mutexPtr的析構函數會在互斥量”引用計數“為0時自動調用tr1::shared_ptr的删除器(unlock)。

複制RAII對象必須一并複制它所管理的資源,是以資源的copying行為決定RAII對象的copying行為。

普遍而常見的RAII類拷貝行為是:抑制拷貝,施行引用計數法。不過其它行為也可能被實作。

  将工廠函數傳回的指針放入智能指針中。

  當我們需要通路原始資源的時候,這時候需要一個函數可将RAII對象(如tr1::shared_ptr)轉換為其所内含之原始資源。有兩種做法可以達成目标:顯示轉換和隐式轉換。

  tr1::shared_ptr和auto_ptr都提供一個get成員函數,用來執行顯示轉換,也就是傳回智能指針内部的原始指針(的複件)。就像所有智能指針一樣, tr1::shared_ptr和auto_ptr也重載了指針取值操作符(operator->和operator*),它們允許隐式轉換至底部原始指針。(即在對智能指針對象實施->和*操作時,實際被轉換為被封裝的資源的指針。)

  是否該提供一個顯示轉換函數(例如get成員函數)将RAII類轉換為其底部資源,或是應該提供隐式轉換,答案主要取決于RAII類被設計執行的特定工作,以及它被使用的情況。

  顯示轉換可能是比較受歡迎的路子,但是需要不停的get,get;而隐式轉換又可能引起“非故意之類型轉換”。

APIs往往要求通路原始資源,是以每一個RAII類應該提供一個“取得其所管理之資源”的方法。

對原始資源的通路可能經由顯示轉換或隐式轉換。一般而言顯示轉換比較安全,但隐式轉換對客戶比較友善。   

  使用了new動态申請了資源,也調用了delete釋放了資源。但這代碼存在“不明确行為”。stringArray對象中的99個不太可能被适當删除,因為它們的析構函數很可能沒被調用。

  當我們使用new,有兩件事情發生:

記憶體被配置設定出來。

針對此記憶體會有一個(或更多)構造函數被調用。

  當你使用delete,也有兩件事發生:

針對此記憶體會有一個(或多個)析構函數被調用,然後記憶體才被釋放。

delete的最大問題在于:即将被删除的記憶體之内究竟有多少對象?這個問題的答案決定了有多少個析構函數必須被調用起來。

  單一對象記憶體布局和數組對象不同,在數組所用的記憶體通常還包括“數組大小”記錄,以便delete知道需要多少次析構函數被調用。讓delete知道記憶體中是否存在一個“數組大小記錄”的辦法是加[],delete會認定指針指向一個數組。

  如果你調用new時使用[],你必須在對應調用delete時也使用[]。如果你調用new時沒有使用[],那麼也不該在對應調用delete時使用[]。

  最好盡量不要對數組形式作typedefs動作。因為這樣容易引起delete操作的“疑惑”。

如果你在new表達式中使用[],必須在相應的delete表達式中也使用[]。如果你在new表達式中不使用[],一定不要在相應的delete表達式中使用[]。

  為了避免資源洩漏的危險,最好在單獨語句内以智能指針存儲newed所得對象。

以獨立語句将newed對象存儲于(置入)智能指針内。如果不這樣做,一旦異常抛出,有可能導緻難以察覺的資源洩漏。

繼續閱讀