資源是這樣一種東西:一旦借助它們所做的事情完成了,必須要将其傳回給系統。如果沒有這樣做,那麼不好的事情就會發生。在 C++ 程式中,最常用的資源是動态配置設定的記憶體(如果配置設定了記憶體但是卻忘記了釋放它們,程式就會遇到一次記憶體洩漏),但是記憶體隻是所需要管理的衆多資源中的一種。其它常見的資源包括檔案主字碼、互斥鎖、以及圖形使用者界面( GUI )中的字型和畫筆、資料庫聯接、網絡套結字。無論是何種資源,在借助它所做的工作完成以後都要将其釋放,這一點是很重要的。 試圖手動将資源管理得井井有條,在任何情況下都是很困難的事情。但當問題轉向異常處理、多路傳回函數、以及當維護程式員在未對其所作的修改有充分了解之前就輕舉妄動時,就會清楚地發現,專門用來解決資源管理問題的方法并不是很充足。 下面的示例中,我們的工作将圍繞一個模拟投資(或者是股票、證券等等)的庫展開,在這個庫中,各種各樣的投資類型都繼承自同一個基類—— Investment : class Investment { ... }; // 投資類型層次結構的基類 供我們使用的庫中,為我們提供具體 Investment 對象是通過工廠函數來實作的: Investment* createInvestment(); // 傳回一個指針,指向 Investment // 層次結構中動态配置設定的對象,調用者必須要将其删除(為簡化代碼省略了參數表) 從上面代碼中的注釋中可以看出,當 createInvestment 的調用者完成對于 createInvestment 函數傳回對象的操作後,這類調用者應負責删除這一對象。請看下邊的代碼,我們用 f 函數來承擔這一責任: void f() { Investment *pInv = createInvestment(); // 調用工廠函數 ... // 使用 pInv delete pInv; // 釋放該對象 } 這看上去可以正常運作,但是 f 可能在一些情況下無法成功的删除來自 createInvestment 的對象。在上述代碼的“ …. ”部分可能存在不成熟的 return 語句。如果這樣的 return 語句得到了執行,那麼程式永遠就不會轉向 delete 語句執行。當在循環語句中使用 createInvestment 和 delete 時,會出現類似的情形,同時這樣的循環也有可能因遇到 continue 或 goto 語句而提前退出。最後,“ ... ”中的一些語句還有可能抛出異常。如果真的有異常抛出,程式同樣也不會達到 delete 。無論 delete 是如何被跳過的,包含 Investment 對象的記憶體都有可能洩露,同時這類對象所控制的資源都有可能得不到釋放。 當然,仔細程式設計就有可能防止這類錯誤發生,但是請想象一下代碼會多麼的不固定——我們需要不停地修改代碼。在軟體維護的過程中,為一個函數添加 return 或 continue 語句可能會對其資源管理政策造成怎樣的影響呢,一些人可能由于不完全了解這一問題就這樣做了。還有更嚴重的,就是 f 函數的“ ... ”部分可能調用了一個這樣的函數:它原先從不會抛出異常,但在其得到“改進”之後,它突然又開始能夠抛出異常了。寄希望于 f 函數總能達到其中的 delete 語句并不可靠。 為了確定 createInvestment 所傳回的資源總能得到釋放,我們需要将這類資源放置在一個對象中,這一對象的析構函數應在程式離開 f 之後自動釋放資源。實際就是将資源放置在對象中,我們可以寄希望于通過調用 C++ 的預設析構函數,進而確定資及時源得到釋放。 許多資源是在堆上動态配置設定的,并且僅僅在單一的程式塊或函數中使用,同時這類資源應該在程式離開這一程式塊或函數之前得到釋放。标準庫中的 auto_ptr 就是為這類情況量身定做的。 auto_ptr 是一個類似于指針的對象(一個智能指針),其析構函數可以自動地對用其所指的内容執行 delete 。以下的代碼描述了如何使用 auto_ptr 來防止 f 潛在的資源洩露。 void f() { std::auto_ptr<Investment> pInv(createInvestment()); // 調用工廠函數 ... // pInv 的用法和原來相同 } // 通過 auto_ptr 的析構函數自動删除 pInv 這一簡單的示例向我們展示了使用對象管理資源的兩大關鍵問題: l 擷取資源後,立即将資源轉交給資源管理對象。 上邊的示例中 , createInvestment 傳回的資源将初始化一個 auto_ptr ,進而實作對這類資源的管理。事實上,使用對象來管理資源的理念通常稱為“資源擷取即初始化”( Resource Acquisition Is Initialization , 簡稱 RAII ),這是因為在同一個語句中擷取一個資源并且初始化一個資源管理對象是很平常的。某些時候擷取資源就是為一個資源管理對象指派,而不是初始化。但是無論是哪種途徑,在擷取資源時,每一個資源都都會立即轉向一個資源管理對象。 l 資源管理對象使用其析構函數來確定資源得到釋放。 由于析構函數是在對象銷毀時自動調用的(比如,當對象将到達其作用域之外),是以不管程式是如何離開一個塊的,資源都會被正确地釋放。如果釋放資源會帶來異常,那麼事情就會變得錯綜複雜。 由于當一個 auto_ptr 被銷毀時,它 自動删除了其所指向的内容,是以永遠不要讓多個 auto_ptr 指向同一個對象,這一點很重要。如果這樣做了,這個對象就會被多次删除,這樣我們的程式就會陷入未知行為的陷阱。為了防止此類問題發生, auto_ptr 有一個不同尋常的特性:如果複制它們(通過拷貝構造函數或者拷貝指派運算符),它們就會被重設為 null ,然後資源的所有權将由複制出的指針獨占! std::auto_ptr<Investment> // pInv1 指向 createInvestment pInv1(createInvestment()); // 所傳回的對象 std::auto_ptr<Investment> pInv2(pInv1); // 現在 pInv2 指向這一對象, // pInv1 被重設為 null pInv1 = pInv2; // 現在 pInv1 指向這一對象 // pInv2 被重設為 null 在這一古怪的複制方法中,由于 auto_ptr 必須僅僅指向一個資源,是以增加了對于資源管理的潛在需求。這意味着 auto_ptr 并不适合于所有動态配置設定的資源。比如說, STL 容器要求其内容的表現出“正常”的拷貝行為,是以 auto_ptr 的容器是不允許使用的。 引用計數智能指針( reference-counting smart pointer , 簡稱 RCSP )是 auto_ptr 的一個替代品。一個 RCSP 是一個這樣的智能指針:它可以跟蹤有多少的對象指向了一個特定的 資源,同時當沒有指針在指向這一資源時,智能指針會自動删除這一資源。可以看出, RCSP 的行為與垃圾回收機很相似。然而, RCSP 與垃圾回收機也不是完全一樣的,它不能夠打斷循環引用(比如說,兩個沒有其它使用者的對象 互相指向對方)。 TR1 的 TR1::shared_ptr 就是一個 RCSP , 于是可以按下面的方式來編寫 f : void f() { ... std::TR1::shared_ptr<Investment> pInv(createInvestment()); // 調用工廠函數 ... // pInv 的用法與前面相同 } // 通過 shared_ptr 的析構函數自動删除 pInv 上面的代碼與使用 auto_ptr 是幾乎完全相同,但是複制 shared_ptr 的行為更加自然: void f() { ... std::TR1::shared_ptr<Investment> pInv1(createInvestment()); // pInv1 指向 createInvestment // 所傳回的對象 std::TR1::shared_ptr<Investment> pInv2(pInv1); // 現在 pInv1 與 pInv2 均指向同一對象 pInv1 = pInv2; // 同上 — 因為什麼都沒有改變 ... } // pInv1 與 pInv2 被銷毀,它們所指向的對象也自動被删除了 由于複制 TR1::shared_ptr 的工作可以“如期進 行 ” ,是以在 auto_ptr 會出現非正統的複制行為的地方,比如 STL 容 器以及其它一些上下文中,這類指針能夠安全地應用。 auto_ptr 和 TR1::shared_ptr 在析構函數中都包含 delete 語句,而不是 delete[] 。這就意味着對于動态配置設定的數組使用 auto_ptr 和 TR1::shared_ptr 不是一個好主意。但是遺憾的是,這樣的代碼會通過編譯: std::auto_ptr <std::string> aps(new std::string[10]); // 壞主意! // 這裡将使用錯誤的删除格式 std::TR1::shared_ptr <int> spi(new int[1024]); // 同樣的問題 有人可能會很吃驚,因為在 C++ 中沒有類似于 auto_ptr 和 TR1::shared_ptr 的方案來解決動态配置設定數組的問題,甚至 TR1 中也沒有。這是因為 vector 和 string 通常都可以代替動态配置設定的數組。如果仍然希望存在類似于 auto_ptr 和 TR1::shared_ptr 的數組類,請參見 Boost 的相關内容。那兒會滿足需求: Boost 提供了 boost::scoped_array 和 boost::shared_array 來處理相關問題。 本條目中建議我們始終使用對象來管理資源。如果我們手動釋放資源(比如使用 delete 而不是使用資源管理類),我們就在做一些錯事。諸如 auto_ptr 和 TR1::shared_ptr 等封裝好的資源管理類通常可以讓遵循本條目的建議變成一件很容易的事情,但是某些情況下,我們的問題無法使用這些預制的類來解決,此時便需要建立自己的資源管理類。但這并沒有想象中那麼難,但是确實需要考慮一些細節問題。 最後說一下,必須指出 createInvestment 的裸指針傳回類型存在着潛在的記憶體洩露問題,因為調用者十分容易忘記在傳回時調用 delete 。(甚至在它們使用 auto_ptr 或 TR1::shared_ptr 來運作 delete 時,他們仍然需要在一個智能指針對象中儲存 createInvestment 的傳回值。)解決這一問題需要改變 createInvestment 的對象。 牢記在心 l 為了避免資源洩露,可以使用 RAII 對象,使用構造函數擷取資源,析構函數釋放資源。 l auto_ptr 或 TR1::shared_ptr 是 兩個常用并且實用的 RAII 類。通常情況 下 TR1::shared_ptr 是更好的選擇,因為它的複制行為更加直覺。複制一個 auto_ptr 将會使其重設為 null 。