3. 所有權的控制與轉移
前一篇文章的資源封裝實現,通過禁用資源封裝類的拷貝和賦值,極大地簡化了這些類的最終**,然而同時也極大的限制了它們的使用範圍。目前他們只能用在區域性的資源管理以及作用常引用在作用域之間傳遞,這無疑是不方便的。我們要嘗試去除這一限制。但是在開始之前,我們先要討論資源的所有權問題,只有明確了資源的所有權,我們才能有效地在不同的作用域之間傳遞資源封裝類,而不會引起混淆。因為一般的經驗顯示,**實現和功能的混淆往往**於概念的含混不清。我們先把基本的東西搞清楚。
對於基於值語義的pod結構體來說,c++完全繼承了c的風格,保留它們的值語義,所以其中並沒有太多的所有權的問題,因為乙個拷貝乙個結構物件自然會完全拷貝乙份所有的結構成員,修改乙個結構例項的成員不會對其他結構例項有任何的影響。這個實現約定極大地簡化了一般**的實現,程式設計師可以自由地把結構物件傳來傳去,就好處處理內建的基本型別一樣。然而,當結構體包含足夠的成員的時候,來恢賦值乙個巨大的結構體的開銷往往難以令人接受,程式設計師們開始使用指標來傳遞巨大的結構例項。然而,使用指標總是導致額外的複雜性,如結構體的構造和銷毀的時機的選擇。c++通過使用合適的封裝技術以及強調所有權的概念,合理的解決了這個問題。
* 資源可以被銷毀,沒有資源洩漏
* 資源可以被及時地銷毀,自動或者手動驅動
* 資源被銷毀以後,不容易被誤用以及重複銷毀
對於第乙個問題,我們可以確信,由於編譯器可以保證物件在析構的時候一定會呼叫析構函式,只要把資源銷毀的操作置於其中即可,而無論該物件是怎麼被析構的,或者是使用者直接呼叫delete操作符,或者該物件離開了其作用域。對於這兩種情況,物件的析構函式都是保證被立即呼叫的,所以上述的第二條也可以得到滿足;當物件析構之後,物件就不存在,任何對該物件的引用都歸於無效,所以也就不能誤用已經被釋放的資源(倒是有可能使用引用誤用被釋放的物件,這是乙個 c++中臭名昭著的區域性物件引用問題)。這裡,你應該可以理解,c**中強烈要求(儘管不是必須)釋放指標後要將指標置零,而c++卻並不推薦。it's really make no sence to well designed c++ code.
這裡,我們不止一次提到作用域,這是乙個很簡單,但是卻十分重要的概念。我們這裡複習一下。c++有多種作用域,我們這裡強調其中幾種。第一種是區域性作用域,那就是定義在乙個快(最小的使用{}包含起來的部分,如if語句之後的{})內的物件的作用域;另一種是類作用域,那就是類中申明的物件(成員變數)的作用域;再有一種就是函式作用域,那是指函式定義中,函式名字後面括號中形參(parameter,實參叫做argument)的作用域;最後就是名字空間作用域和全域性作用域。全域性作用域是指不包含在任何的{}之內的名字的作用域,名字空間是鬆散的構造,這裡把它一塊歸於全域性作用域討論,因為它們有一些共同的屬性,但是請明確二者有嚴格的區別。
另一方面要複習的概念是物件的生命週期。這是任何物件的一種執行期屬性,乙個物件的宣告週期開始於:
1. 物件需要的儲存(合適的大小並且對齊)完全獲得
2. 如果物件有非平凡建構函式(不是編譯器自動生成的),則需要該建構函式執行完畢。
它的生命週期結束於:
1. 如果物件有非平凡析構函式,析構函式被執行完畢
2. 物件所佔的儲存被釋放。
我們只考慮這種最簡單的情況,對於陣列以及派生類物件,細節上還有差異。
最後乙個概念(保證是最後乙個)是物件儲存持續期。有三種:靜態物件儲存持續期,動態物件儲存持續期以及自動物件儲存持續期。動態物件儲存持續期對應於使用 operator new獲得的物件,使用自動物件儲存持續期對應於區域性物件,初次之外的全部物件都是靜態物件儲存持續期的。
有了這三個概念,我們可以說,由於資源所有權的物件需要在其生命週期終止的時候保證釋放其資源。屬性是靜態物件儲存持續期的物件都由執行期系統負責構造和釋放;而屬性是動態物件儲存持續期的則要求使用者手動呼叫operator delete。所以和資源管理物件關係最密切的就是那些屬性是自動物件儲存持續期的物件,我們知道,這是區域性物件;我們又知道,區域性物件的作用域是區域性物件所在的塊。當執行流退出這個塊的時候,根據作用域原則以及物件的生命週期定義,其中的所有區域性物件都會被保證釋放。這樣,環環相扣,最終保證了我們前面說的資源所有權管理的三大要素!
由此,我們也可以順便了解到:
1. 全部物件不宜使用資源封裝類,儘管這類物件的生命週期固定,但是其構造順序卻是不確定的。如果去阿奴資源物件存在依賴關係,其效果則完全不能保證;
明確了資源的所有權之後,我們就要著手解決資源的轉移問題。首先,我們先把我們的需求清理一下。
所謂資源轉移,那就意味著原有的物件放棄對資源的擁有(釋放的權利),而新的物件擁有該資源。那麼原來的物件放棄資源所有權之後,那還能繼續使用資源嗎?這真是個問題呀。一派意見是當然可以,放棄擁有權和使用資源與否是兩碼事,我們不是可以租房子住嘛!另一派則以為不妥,那原來的物件怎麼知道它使用的這個被其他人擁有的物件不會被悄悄被釋放呢?可以繼續使用倒是方便,但是它引入了更**煩。所以對於資源的轉移,必須禁止原來的物件可以繼續使用。
這時候有人提問題了,為什麼要轉移資源的所有權?使用者需要的是可以方便地使用資源,而不關心到底最後到底是誰來釋放的,只要你保證釋放了就好,不是嗎?大家仔細一想,確實如此,我們應該強調的是資源「控制」而不是「轉移」。通過有效的資源控制,我們可以放心大膽地讓使用者隨意使用資源封裝物件,而僅僅在真正需要的時候才提供資源的轉換甚至複製操作。由於資源轉換和複製的開銷的***都很大,必須有使用者的明確命令才可以進行。這裡,所有的資源物件都共享資源所有權,只要有多於乙個物件存在,我們就不知道到底誰會最後負責釋放物件。
總結一下:
資源轉移:資源所有權在物件之間的傳遞。原來物件在釋放所有權之後不再刻意繼續使用資源。該操作的開銷很小
資源複製:複製保護的資源,並使用這個副本來構建另乙個資源管理類物件。該操作因為涉及資源的複製,開銷可能會很大
資源控制:通過提供有效的控制介面,而是資源管理類物件具有值語義。資源管理類負責協調有那個物件最終釋放資源。這種情況下,可以看做是用多個物件共享所有權。物件複製的開銷很小。
4. 庫支援
資源的控制和管理討論的足夠多了,看看c++標準庫給我們提供了什麼。
在 c++97/2003中,c++通過std::auto_ptr,提供了資源轉移的支援。我們已經知道,資源轉移儘管完全使用了資源管理的基本技術,但是它帶來的問題也不少,所以自從std::auto_ptr出生的第一天起,各方的爭論就不斷。保守的一方堅定地認為這是乙個叛逆者,因為它完全不相容於 c++容器,不相容於人們的日常使用習慣,應該嚴謹在**中使用;而集、激進的一方則認為這是標準唯一支援的資源管理元件,在保證移植性並且有效使用資源管理技術的唯一標準方案。儘管它的行為違反直覺,但是這也初始人們真正考慮資源的轉移問題。而且,合適地限制其使用範圍,如僅僅區域性使用,可以極大地簡化異常安全的**的實現。幾年爭論下來,一直的意見沒有達成,卻促進了c++標準庫在這方面的進步。c++0x通過引入了在boost庫中廣泛使用的 shared_ptr,提供了基於值語義的資源控制支援。這樣,c++資源管理技術的支援終於完善!shared_ptr可以用於任何的標準容器,提供了可疑媲美基本物件的操作介面(由於其基於引用計數的實現,在某些特殊的情況下,如使用shared_ptr的雙鏈表,會導致鍊錶中最後乙個節點物件不能被釋放。這個問題是通過引入乙個叫做weak_ptr的構造來打破迴圈依賴而解決的)。
由於shared_ptr可以接受另乙個額外的模板引數作為資源釋放的deleter,shared_ptr不僅僅可以用於簡單堆物件的管理,還可以用於任何使用基於raii技術的資源封裝類。不行的是 c++0x還得假以時日才能正式發布,我們離標準相容的資源管理支援還有一段小小的距離。
儘管如此,我們可以說,c++從語言核心層到標準庫,都(逐漸)提供了基本完善的資源管理構造技術。我們只是需要有效地使用它。使用這些元件或者技術把那個不是很難,困難的時候深入理解其背後的思想並清除其要解決的問題。真正有了資源管理,我們才能深入到另乙個使得c++大放異彩的特性:基於異常的錯誤管理。沒有基於 raii技術的資源管理,就不能有效地進行錯誤處理,已經成為業界的常識。
5. 自定義的實現
在c++0x之前,要使用全套的資源管理技術,我們有幾個選擇,乙個是使用boost的智慧型指標庫,具體可以參考來自bj?rn karlsson的「beyond the c++ standard library: an introduction to boost「。現在各個主流的c++編譯器都提供了對c++0x庫部分tr1的支援,所以使用其中的tr1::shared_str也正當時,具體看以參考來自pete becker的」the c++ standard library extensions: a tutorial and reference「。有了shared_ptr的支援,再加上我們原先的資源封裝類,就可以所向披靡了。
不過還有另外一種選擇,自己實現基於值語義的資源封裝類。這個要求我們從頭開始做一些工作,但是卻解除了對shared_ptr元件的依賴。共享資源實現的基本原理是使用引用計數技術。但是我們不想對於任何乙個資源類都實現一套這樣的機制。理想的情況,我們實現乙個類,然後所有的類都可以從其繼承,從而自動獲得基於引用計數的資源共享。這是不可能的,因為物件的析構過程中,派生類首先析構;而派生類析構後,基於多型的過載基礎也就不存在了,所以基類在析構函式裡無法呼叫派生類的實現。所以實現情況要稍微複雜一些。
這裡先給出乙個比較簡單的實現。它基於:
1. 每乙個資源管理類包含乙個引用計數物件。該物件負責維護物件的計數。
2. 在資源管理類析構的時候,查詢引用計數,如果為1,則釋放資源,然後執行一般析構函式
3. 是賦值操作符中,也需要查詢當前資源的引用計數,如果為1,則釋放當前的資源,然後執行一般複製操作符
因為引用計數物件是資源管理物件的一部分,其他操會自然導致引用計數的增減。無需考慮。
下面是使用**;
可以注意到,我們要處理的函式只有析構函式和賦值運算子。下面是引用計數類的乙個直觀的實現
細心的讀者可能會注意到, 這個實現和「ruminates on c++」中提供的乙個實現相像,不必奇怪,就是從其中衍生出來的:)筆者也建議任何乙個嚴肅的c++工程師仔細讀這本小品書。
人生魯棒性的WWH
悟已往之不諫,知來者之可追。實迷途其未遠,覺今是而昨非。歸去來兮辭 陶淵明 可以未雨綢繆,但不應為此焦慮 接受最壞的可能,爭取最好的結果 改變自己對現實的認知 判斷和相應的決策,積極的人生態度和樂觀精神。開誠布公不代表你一定能獲得想要的結果,也不代表你能跨過那些障礙,但可以讓你更直接的得到答案,如果...
網路模型的魯棒性(結合例項)與提公升魯棒性的方法
1.定義 在統計學領域和機器學習領域,對異常值也能保持穩定 可靠的性質,稱為魯棒性。比如說,計算機軟體在輸入錯誤 磁碟故障 網路過載或有意攻擊情況下,能否不宕機 不崩潰,就是該軟體的魯棒性。所謂 魯棒性 是指控制系統在一定 結構,大小 的引數攝動下,維持某些效能的特性。有乙個與魯棒性很相似的概念叫模...
積體電路的魯棒性
1.擾動 1 工藝擾動 2 電源電壓 3 工作溫度 這三個擾動又稱為pvt。你的目標是必須設計乙個電路使它在這三個引數的所有的極端情況下都能可靠工作。電源電壓除隨時間變化外還在整個晶元上變化。隨溫度上公升,漏極電流下降。工藝擾動 對於器件,最主要的擾動是溝道長度l和閾值電壓vt。溝道長度擾動是由光刻...