原創(chuàng)|行業(yè)資訊|編輯:郝浩|2014-12-26 15:35:10.000|閱讀 4032 次
概述:學(xué)習(xí)和使用過C++的人幾乎都曾經(jīng)聽說過下面的五個關(guān)于C++的描述,并且對這些話篤信不已,那么現(xiàn)在的情況是怎么樣的呢?本文的作者——C++之父Bjarne Stroustrup將會對這些觀點作逐一回?fù)簟1酒獮橹衅?,探討其中的第三個觀點。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關(guān)鏈接:
學(xué)習(xí)和使用過C++的人幾乎都曾經(jīng)聽說過下面的五個關(guān)于C++的描述,并且對這些話篤信不已,那么現(xiàn)在的情況是怎么樣的呢?本文的作者——C++之父Bjarne Stroustrup將會對這些觀點作逐一回?fù)簟?/p>
以下的這五個觀點盛行于C++多年:
如果你還對這些觀點深信不已,那么這篇文章可以給你一些重新認(rèn)識。這些觀點在特定的時間對于某些人、某些工作來說是正確的。但是對于今天的C++,隨著ISO C++11標(biāo)準(zhǔn)的編譯器和工具的廣泛使用,這些觀點都需要被重新認(rèn)識。
接上一篇,這一篇里我們將圍繞“對于可靠的軟件,垃圾回收機(jī)制必不可少。”的觀點進(jìn)行探討。
對于回收未使用的內(nèi)存這份工作,垃圾回收做得不錯但卻不夠完美。它并非靈丹妙藥。內(nèi)存可以被間接引用并且許多資源并非單純的內(nèi)存。來看這個例子:
這里Filter的構(gòu)造函數(shù)會開啟兩個用于數(shù)據(jù)存儲的文件(file)。完成這項工作以后,F(xiàn)ilter從輸入文件執(zhí)行輸入任務(wù)并將產(chǎn)生的輸出結(jié)果保存到輸出文件里。 這些任務(wù)包括硬連接到Filter,作為匿名(lambda)函數(shù),提供一個可能具有覆蓋虛函數(shù)派生類的函數(shù)。在談及資源管理時這些細(xì)節(jié)并不重要。我們可以這樣創(chuàng)建Filter:
從資源管理的角度來看,這里的問題是如何關(guān)閉文件以及對與輸入輸出流相關(guān)聯(lián)的對象資源進(jìn)行回收重用。
在許多種依托于垃圾回收的語言和系統(tǒng)里,常見解決方案是放棄使用delete(它很容易在編程過程中被人遺忘,從而導(dǎo)致內(nèi)存泄漏)和析構(gòu)函數(shù)(被垃圾回收后的語言中盡量少用析構(gòu)函數(shù)和不用finalizer,因為它們在邏輯上令人捉摸不透并經(jīng)常破壞性能)。垃圾回收器可以回收所有的內(nèi)存資源,但是我們還需要使用手動操作(通過編寫代碼的方式)來關(guān)閉文件并釋放任何與數(shù)據(jù)流相關(guān)的非內(nèi)存資源(比如鎖)。因此雖然內(nèi)存被自動完全回收了,但是由于其它資源是手動管理的,內(nèi)存的錯誤和泄漏仍有可能發(fā)生。
被C++推薦和使用的方法是依靠析構(gòu)函數(shù)來處理資源回收的問題。值得一提的是,這些被構(gòu)造函數(shù)獲取的資源是通過RAII(“資源獲取即初始化”)這一簡單而通用的技術(shù)來處理的。在user()中,用于flt的析構(gòu)函數(shù)隱式調(diào)用了用于輸入輸出流(IS及OS)的析構(gòu)函數(shù)。這些析構(gòu)函數(shù)依次關(guān)閉文件并釋放與數(shù)據(jù)流相關(guān)的資源。而delete對*p會做同樣的操作。
擁有豐富的現(xiàn)代C++開發(fā)經(jīng)驗的程序員會注意到user()非常笨拙且容易產(chǎn)生錯誤,而采用下面的編寫方式會更好:
現(xiàn)在當(dāng)user()退出后*p需要被隱式釋放。程序員不能忘記這項操作。與內(nèi)置的“裸”指針不同的是,智能指針unique_ptr是一個用于確保資源釋放掉后就不再需要運行時間和內(nèi)存空間等系統(tǒng)開銷的標(biāo)準(zhǔn)庫類。
然而,我們?nèi)匀荒軌蚩吹絥ew。這個解決方案有點冗長(Filter類型重復(fù)了),并且由于結(jié)構(gòu)被普通指針(使用的new)和智能指針(在這里是unique_ptr)分拆開而使某些重要的優(yōu)化丟失。我們可以使用一個C++14的幫助函數(shù)make_unique來進(jìn)行改善,它能夠構(gòu)造一個指定類型的對象并返回一個指向它的unique_ptr指針:
除非出現(xiàn)需要第二個具有指針語義的Filter的情況(不太可能),否則這段代碼將會更好:
最后的一個版本比原來的更加簡短、清晰和快速。
Filter的析構(gòu)函數(shù)做了什么呢?它釋放了屬于Filter的資源。也就是說,它關(guān)閉了文件(通過調(diào)用它們的析構(gòu)函數(shù))。事實上,這項工作是通過隱式的方式完成的,所以除了Filter需要的一些東西,我們可以去掉Filter析構(gòu)函數(shù)的顯式聲明并讓編譯器來處理這一切。因此,我只需要這樣編寫:
這樣比大多數(shù)擁有垃圾回收機(jī)制的語言(如Java或者C#)的編寫都要簡單,而且也不會因為程序員的健忘而導(dǎo)致內(nèi)存泄漏。它比其它的替代方案也要快速的多(無需模擬自由/動態(tài)內(nèi)存的使用且不需要運行垃圾回收器)。值得一提的是,相對于手動操作的方法RAII還降低了資源的滯留時間。
這是理想的資源管理方法。它處理的不僅是內(nèi)存,還包括一般(非內(nèi)存)資源,比如文件句柄、線程句柄以及鎖等。但這樣就夠了么?對于那些需要從一個函數(shù)傳遞到另外一個函數(shù)的對象又該怎么辦呢?對于那些沒有明顯的單一所有者的對象又該怎么辦呢?
讓我們首先來考慮將對象(所包含的信息)從一個作用域轉(zhuǎn)移到另一個的問題。這個問題的關(guān)鍵在于在不使用copy或易錯指針等需要影響系統(tǒng)性能的情況下如何從作用域之外獲得大量關(guān)于所需對象的信息。傳統(tǒng)的方法是使用一個指針:
現(xiàn)在負(fù)責(zé)刪除對象的是誰?在這個簡單的例子中,很明顯是make_X()的調(diào)用者,但在通常情況下這個答案是不明確的。假如make_X()為了將系統(tǒng)開銷降低最小而保留了對象緩存呢?假如user()將指針傳遞給了一些other_user()呢?這種方法產(chǎn)生混亂的可能性很大并且也容易產(chǎn)生內(nèi)存泄漏。
我可以使用shared_ptr或者unique_ptr來明確所創(chuàng)建對象的所有權(quán)。例如:
但是為什么非要使用一個指針(智能指針或者一般指針)呢?我通常都不希望使用指針,因為指針的使用與常規(guī)的對象引用不合拍。例如,一個Matrix加法函數(shù)創(chuàng)建了一個包含2個參數(shù)的新對象(求和),但如果返回一個指針則會導(dǎo)致代碼變得非常奇怪:
那個*的位置應(yīng)該是需要的求和結(jié)果,而不是一個指向這個結(jié)果的指針。在很多時候,我真正想獲取的是一個對象,而不是指向?qū)ο蟮闹羔槨6鄶?shù)情況下,獲取對象都會很簡單,特別是對于那些小型對象,只需要簡單的copy就可以了,根本不需要考慮使用指針:
另一方面,一個包含大量數(shù)據(jù)信息的對象通常會處理大部分那樣的數(shù)據(jù)。比如istream,string,vector,list和thread。它們只是使用了幾句關(guān)于數(shù)據(jù)的簡單命令就可以確保潛在的大量數(shù)據(jù)的合理訪問。讓我們再來看看Matrix加法,我們希望的是
我們可以很容易用這種實現(xiàn)(創(chuàng)建臨時對象函數(shù)):
在默認(rèn)的情況下,程序會把res(臨時對象)的元素copy到r,但隨后res會被銷毀,持有這些元素所占用的內(nèi)存也會被釋放,我們考慮到了一種無需copy(C++的設(shè)計目標(biāo)就是盡量少分配內(nèi)存)的方法:直接“竊取”這些元素。從第一天學(xué)習(xí)C++的初學(xué)者到老手,每一個人都想過要這么做,但這種方法很難實現(xiàn)且技術(shù)還沒有得到廣泛理解。C++11的出現(xiàn)使這種構(gòu)想成為了現(xiàn)實。它支持“竊取對象信息(steal the representation)”的理念——通過move句柄的形式轉(zhuǎn)移對象所有權(quán)(即轉(zhuǎn)移對象所包含信息)。來看看下面這個簡單的2維雙重Matrix函數(shù):
copy操作可通過引用(&)參數(shù)來識別的,同樣的,move操作可通過右值引用(&&) 參數(shù)來識別。move操作可以用來“竊取”對象的信息并遺留下一個“空對象”。對于Matrix來說,這就意味著是這樣的:
它的機(jī)制是這樣的:當(dāng)編譯器看到了return res,它就明白可以把res銷毀了。也就是說,res在返回之后就不會再使用了。因此,編譯器會立刻應(yīng)用一個move構(gòu)造函數(shù)而不是copy構(gòu)造函數(shù)來轉(zhuǎn)移返回的值。通過以下的形式:
在operator+()中的res會成為空對象,然后交由析構(gòu)函數(shù)來善后,而res中的元素現(xiàn)在已經(jīng)歸r所有。將對象包含的信息從函數(shù)operator+()提取出來放進(jìn)調(diào)用的變量中,我們已經(jīng)達(dá)成了獲取元素(可能是上百萬字節(jié)的內(nèi)存)的結(jié)果,并且我們只使用了最小的成本(也就是差不多四行用于分配的代碼)。
老道的C++用戶會指出,在某些情況下,好的編譯器能夠完全清除掉return上所copy的信息(在本例中會保存關(guān)于move的四行代碼和調(diào)用的析構(gòu)函數(shù))。然而,這是對實現(xiàn)的依賴,我不希望基礎(chǔ)編程技術(shù)的性能還要由每個獨立編譯器的聰明程度來決定。此外,能夠清除掉copy信息的編譯器也能夠很輕松的把move給抹掉。我們這里的就有一個用于減小把大量信息從一個作用域copy到另外一個的復(fù)雜性和所產(chǎn)生花費的簡單、可靠、通用的方法。
通常情況下,我們甚至不需要定義所有的這些copy和move操作。如果一個類中缺乏所需的成員,我們可以依靠編譯器所生成的默認(rèn)操作,比如:
這個版本的Matrix運行起來與上個版本很相似,除了稍微提升了對錯誤的處理和有一個更多一些的陳述(vector通常只有3行代碼)
對于那些不是句柄的對象呢?假如它們很小,就象一個int或者一個雙double類型complex<double>那樣,則無須擔(dān)心。否則,需要使用nique_ptr或shared_ptr這樣的智能指針來處理它們并進(jìn)行返回操作。注意,不要加入“裸”指針new和delete。
不幸的是,就象我舉例的Matrix類一樣,某些類并不是ISO C++標(biāo)準(zhǔn)庫的一部分,但是它的其中一部分還是可用的(開源和面向商業(yè)的)。例如,在網(wǎng)上搜索“Origin Matrix Sutton”,你可以看見在我的書The C++ Programming Language (Fourth Edition)的第29章在討論如何設(shè)計這樣的一個矩陣。
在關(guān)于垃圾回收的討論中,經(jīng)常會看到并不是每一個對象都對應(yīng)唯一的所有者。這意味著我們必須確保當(dāng)對象的最后一個引用消失后,該對象是否已經(jīng)被銷毀/釋放。在這個模型里,我們必須使用一個機(jī)制來確保當(dāng)最后一個所有者被銷毀后這個對象也會隨之被銷毀。也就是說,我們需要一個共享所有權(quán)的形式。例如,我們有一個同步隊列sync_queue,用于任務(wù)之間的通信。提供者(producer)和使用者(consumer)都被賦予了一個指向sync_queue的指針:
我假定task1、task2、iqueue和oqueue已經(jīng)在其它地方被定義了,在這里我使用了detatch()來讓線程的生存周期比創(chuàng)建線程的作用域更長。你可能會想到多任務(wù)管道和sync_queues。然而,在這里我感興趣的只有一個問題:“是誰刪除了startup()中所創(chuàng)建的sync_queue?”以書面文字來說,這問題這么提會更好:“最后使用sync_queue的是誰?”這是經(jīng)典的垃圾回收調(diào)用案例。垃圾回收的原型就是計算指針:持續(xù)對使用對象計數(shù),當(dāng)計數(shù)歸零則刪除該對象。(當(dāng)有一個指針指向自己時計數(shù)值加1;當(dāng)刪除一個指向自己的指針時,計數(shù)值減1,如果計數(shù)值減為0,說明已經(jīng)不存在指向該對象的指針了,則可以安全銷毀)?,F(xiàn)在許多語言的垃圾回收機(jī)制都是以此為藍(lán)本發(fā)展的而在C++11里shared_ptr就是使用的這種機(jī)制。上面的例子可變成:
用于task1和task2的析構(gòu)函數(shù)可以銷毀它們的shared_ptrs(在大多數(shù)優(yōu)秀的設(shè)計當(dāng)中都會非常隱蔽的干這項工作),兩者中較晚完成的會同時對sync_queue進(jìn)行銷毀。
這個方法簡單且合理高效。它意味著一個運行復(fù)雜的系統(tǒng)并一定需要垃圾回收器。重要的是,它不僅可以回收與sync_queue相關(guān)的內(nèi)存資源,還能夠回收sync_queue中用于管理不同任務(wù)的多線程同步性的同步對象(互斥對象、鎖等)。這種方法不僅適用于內(nèi)存管理,還適合一般的資源管理。“隱藏”的同步對象準(zhǔn)確處理前面例子中文件句柄和數(shù)據(jù)流緩沖器所處理的工作。
我們可以嘗試通過在某些封裝任務(wù)的作用域中引入一個唯一所有者來替代使用shared_ptr,當(dāng)這樣做起來并不一定簡單,因此C++11提供了unique_ptr(用于唯一所有權(quán))和shared_ptr(用于共享所有權(quán))。
前面,我只談?wù)摿死厥张c資源管理的關(guān)系。在類型安全方面,垃圾回收也影響重大。只要我們有一個明確的delete操作,它就有可能被誤用。例如:
不要這樣做,在一般的用戶代碼上使用“裸指針”delete是危險且多余的。讓delete遠(yuǎn)離字符串、輸出流、線程、unique_ptr和shared_ptr這樣的資源管理類。在這些地方,delete需要與new謹(jǐn)慎配用來以確保無害。
對于資源管理,我認(rèn)為垃圾回收應(yīng)該作為最后的選擇,而不是作為“解決方案”或者理念:
這樣的策略很完美么?不,但是至少它是簡單適用的?;趥鹘y(tǒng)垃圾回收的策略并不完美,它并不能直接解決非內(nèi)存資源的問題。
前一篇我們探討了“要了解C++,你必須先學(xué)習(xí)C語言。”和“C++是一門面向?qū)ο蟮恼Z言。”的觀點,在下一篇我們將探討最后兩個觀點“為了提高效率,你必須編寫底層代碼。”和“C++只對大型復(fù)雜的項目有用。”
本文翻譯自,作者為:C++之父Bjarne Stroustrup
本文譯者為慧都控件網(wǎng)——回憶和感動,轉(zhuǎn)載請注明:本文轉(zhuǎn)載自慧都控件網(wǎng)
【年終大促 巔峰盛“慧”】促銷火熱進(jìn)行中iPhone 6 Plus、iPhone 6、iPad Air等你拿 <<<<點擊查看
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請郵件反饋至chenjj@fc6vip.cn