條款1:指針與引用的區別
二者之間的區別是:在任何情況下都不能用指向空值的引用,而指針則可以;指針可以被重新賦值以指向另一個不同的對象,但是引用則總是指向在初始化時被指定的對象,以后不能改變。
在以下情況下使用指針:一是存在不指向任何對象的可能性;二是需要能夠在不同的時刻指向不同的對象。
在以下情況使用引用:總是指向一個對象且一旦指向一個對象之后就不會改變指向;重載某個操作符時,使用指針會造成語義誤解。
條款2:盡量使用C++風格的類型轉換
static_cast:功能上基本上與C風格的類型轉換一樣強大,含義也一樣。但是不能把struct轉換成int類型或者把double類型轉換成指針類型。另外,它不能從表達式中去除const屬性。
const_cast:用于類型轉換掉表達式的const或volatileness屬性。但是不能用它來完成修改這兩個屬性之外的事情。
dynamic_cast:用于安全地沿著類的繼承關系向下類型轉換。失敗的轉換將返回空指針或者拋出異常。
reinterpret_cast:這個操作符被用于的類型轉換的轉換結果時實現時定義。因此,使用它的代碼很難移植。最普通的用途就是在函數指針之間進行轉換。
條款3:不要使用多態性數組
多態和指針算法不能混合在一起使用,所以數組和多態也不能用在一起。
數組中各元素的內存地址是數組的起始地址加上之前各個元素的大小得到的,如果各元素大小不一,那么編譯器將不能正確地定位元素,從而產生錯誤。
條款4:避免無用的缺省構造函數
沒有缺省構造函數造成的問題:通常不可能建立對象數組,對于使用非堆數組,可以在定義時提供必要的參數。另一種方法是使用指針數組,但是必須刪除數組里的每個指針指向的對象,而且還增加了內存分配量。
提供無意義的缺省構造函數會影響類的工作效率,成員函數必須測試所有的部分是否都被正確的初始化。
條款5:謹慎定義類型轉換函數
缺省的隱式轉換將帶來出乎意料的結果,因此應該盡量消除,使用顯式轉換函數。通過不聲明運算符的方法,可以克服隱式類型轉換運算符的缺點,通過使用explicit關鍵字和代理類的方法可以消除單參數構造函數造成的隱式轉換。
條款6:自增和自減操作符前綴形式與后綴形式的區別
后綴式有一個int類型參數,當函數被調用時,編譯器傳遞一個0作為int參數的值傳遞給該函數。可以在定義時省略掉不想使用的參數名稱,以避免警告信息。
后綴式返回const對象,原因是 :使該類的行為和int一致,而int不允許連續兩次自增后綴運算;連續兩次運算實際只增一次,和直覺不符。
前綴比后綴效率更高,因為后綴要返回對象,而前綴只返回引用。另外,可以用前綴來實現后綴,以方便維護。
條款7:不要重載&&,||,或者“,”
對 于以上操作符來說,計算的順序是從左到右,返回最右邊表達式的值。如果重載的話,不能保證其計算順序和基本類型想同。操作符重載的目的是使程序更容易閱 讀,書寫和理解,而不是來迷惑其他人。如果沒有一個好理由重載操作符,就不要重載。而對于&&,||和“,”,很難找到一個好理由。
條款8:理解各種不同含義的new和delete
new操作符完成的功能分兩部分:第一部分是分配足夠的內存以便容納所需類型的對象;第二部分是它調用構造函數初始化內存中的對象。new操作符總是做這兩件事,我們不能以任何方式改變它的行為。
我們能改變的是如何為對象分配內存。new操作符通過調用operator new來完成必需的內存分配,可以重寫或重載這個函數來改變它的行為。可以顯式調用operator來分配原始內存。
如果已經分配了內存,需要以此內存來構造對象,可以使用placement new,其調用形式為new(void* buffer)class(int size)。
對于delete來說,應該和new保持一致,怎樣分配內存,就應該采用相應的辦法釋放內存。
operator new[]與operator delete[]和new與delete相類似。
條款9:使用析構函數防止資源泄漏
使用指針時,如果在delete指針之前產生異常,將會導致不能刪除指針,從而產生資源泄漏。
解決辦法:使用對象封裝資源,如使用auto_ptr,使得資源能夠自動被釋放。
條款10:在構造函數中防止資源泄漏
類中存在指針時,在構造函數中需要考慮出現異常的情況:異常將導致以前初始化的其它指針成員不能刪除,從而產生資源泄漏。解決辦法是在構造函數中考慮異常處理,產生異常時釋放已分配的資源。最好的方法是使用對象封裝資源。
條款11:禁止異常信息傳遞到析構函數外
禁止異常傳遞到析構函數外的兩個原因:第一能夠在異常傳遞的堆棧輾轉開解的過程中,防止terminate被調用;第二它能幫助確保析構函數總能完成我們希望它做的所有事情。
解決方法是在析構函數中使用try-catch塊屏蔽所有異常。
條款12:理解“拋出一個異常”與“傳遞一個參數”或“調用一個虛函數”間的差異
有 三個主要區別:第一,異常對象在傳遞時總被進行拷貝。當通過傳值方式捕獲時,異常對象被拷貝了兩次。對象作為參數傳遞給函數時不需要被拷貝;第二,對象作 為異常被拋出與作為參數傳遞給函數相比,前者類型轉換比后者少(前者只有兩種轉換形式:繼承類與基類的轉換,類型化指針到無類型指針的轉換);最后一點, catch子句進行異常類型匹配的順序是它們在源代碼中出現的順序,第一個類型匹配成功的擦他處將被用來執行。當一個對象調用一個虛函數時,被選擇的函數 位于與對象類型匹配最佳的類里,急事該類不是在源代碼的最前頭。
條款13:通過引用捕獲異常
有三個選擇可以捕獲異常:第一、指 針,建立在堆中的對象必需刪除,而對于不是建立在堆中的對象,刪除它會造成不可預測的后果,因此將面臨一個難題:對象建立在堆中還是不在堆中;第二、傳 值,異常對象被拋出時系統將對異常對象拷貝兩次,而且它會產生“對象切割”,即派生類的異常對象被作為基類異常對象捕獲時,它的派生類行為就被切割調了。 這樣產生的對象實際上是基類對象;第三、引用,完美解決以上問題。
條款14:審慎使用異常規格
避免調用unexpected函數 的辦法:第一、避免在帶有類型參數的模板內使用異常規格。因為我們沒有辦法知道某種模板類型參數拋出什么樣的異常,所以不可能為一個模板提供一個有意義的 異常規格;第二、如果在一個函數內調用其它沒有異常規格的函數時應該去除這個函數的異常規格;第三、處理系統本身拋出的異常。可以將所有的 unexpected異常都被替換為自定義的異常對象,或者替換unexpected函數,使其重新拋出當前異常,這樣異常將被替換為 bad_exception,從而代替原來的異常繼續傳遞。
很容易寫出違反異常規格的代碼,所以應該審慎使用異常規格。
條款15:了解異常處理的系統開銷
三 個方面:第一、需要空間建立數據結構來跟蹤對象是否被完全構造,還需要系統時間保持這些數據結構不斷更新;第二、try塊。無論何時使用它,都得為此付出 代價。編譯器為異常規格生成的代碼與它們為try塊生成的代碼一樣多,所以一個異常規格一般花掉與try塊一樣多的系統開銷。第三、拋出異常的開銷。因為 異常很少見,所以這樣的事件不會對整個程序的性能造成太大的影響。
條款16:牢記80─20準則
80─20準則說的是大約20%的代碼使用了80%的程序資源,即軟件整體的性能取決于代碼組成中的一小部分。使用profiler來確定程序中的那20%,關注那些局部效率能夠被極大提高的地方。
條款17:考慮使用懶惰計算法
懶惰計算法的含義是拖延計算的時間,等到需要時才進行計算。其作用為:能避免不需要的對象拷貝,通過使用operator[]區分出讀寫操作,避免不需要的數據庫讀取操作,避免不需要的數字操作。但是,如果計算都是重要的,懶惰計算法可能會減慢速度并增加內存的使用。
條款18:分期攤還期望的計算
核心是使用過度熱情算法,有兩種方法:緩存那些已經被計算出來而以后還有可能需要的值;預提取,做比當前需要做的更多事情。
當必須支持某些操作而不總需要其結果時,可以使用懶惰計算法提高程序運行效率;當必須支持某些操作而其結果幾乎總是被需要或不止一次地需要時,可以使用過度熱情算法提高程序運行效率。
條款19:理解臨時對象的來源
臨時對象產生的兩種條件:為了是函數成功調用而進行隱式類型轉換和函數返回對象時。
臨時對象是有開銷的,因此要盡可能去消除它們,然而更重要的是訓練自己尋找可能建立臨時對象的地方。在任何時候只要見到常量引用參數,就存在建立臨時對象而綁定在參數上的可能性。在任何時候只要見到函數返回對象,就會有一個臨時對象被建立(以后被釋放)。
條款20:協助完成返回值優化
應當返回一個對象時不要試圖返回一個指針或引用。
C+ +規則允許編譯器優化不出現的臨時對象,所有最佳的辦法莫過于:retrun Ratinal(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator())。這種優化是通過使用函數的retuan location(或者用在一個函數調用位置的對象來替代),來消除局部臨時對象,這種優化還有一個名字:返回值優化。
條款21:通過重載避免隱式類型轉換
隱式類型轉換將產生臨時對象,從而帶來額外的系統開銷。
解決辦法是使用重載,以避免隱式類型轉換。要注意的一點是在C++中有一條規則是每一個重載的operator必須帶有一個用戶定義類型的參數(這條規定是有道理的,如果沒有的話,程序員將能改變預定義的操作,這樣做肯定吧程序引入混亂的境地)。
另外,牢記80─20規則,沒有必要實現大量的重載函數,除非有理由確信程序使用重載函數后整體效率會有顯著提高。
條款22:考慮用運算符的賦值形式取代其單獨形式
運算符的賦值形式不需要產生臨時對象,因此應該盡量使用。對運算符的單獨形式的最佳實現方法是return Rational(lhs) += rhs;這種方法將返回值優化和運算符的賦值形式結合起來,即高效,又方便。
條款23:考慮變更程序庫
程序庫必須在效率和功能等各個方面有各自的權衡,因此在具體實現時應該考慮利用程序庫的優點。例如程序存在I/O瓶頸,就可以考慮用stdio替代iostream。
條款24:理解虛擬函數、多繼承、虛基類和RTTI所需的代價
虛函數所需的代價:必須為每個包含虛函數的類的virtual table留出空間;每個包含虛函數的類的對象里,必須為額外的指針付出代價;實際上放棄了使用內聯函數。
多繼承時,在單個對象里有多個vptr(一個基類對應一個)。它和虛基類一樣,會增加對象體積的大小。
RTTI能讓我們在運行時找到對象和類的有關信息,所以肯定有某個地方存儲了這些信息,讓我們查詢。這些信息被存儲在類型為type_info的對象里,可以通過typeid操作符訪問到一個類的typeid對象。通常,RTTI被設計為在類的vbtl上實現。
條款25:將構造函數和非成員函數虛擬化
構 造函數的虛擬化看似無意義,但是在實際當中有一定的用處。例如,在類中構建一個虛擬函數,其功能僅僅是實現構造函數,就可以對外界提供一組派生類的公共構 造接口。虛擬拷貝構造函數也是可以實現的,但是要利用到最近才被采納的較寬松的虛擬函數返回值類型規則。被派生類重定義的虛擬函數不用必須與基類的虛擬函 數具有一樣的返回類型。
具有虛擬行為的非成員函數很簡單。首先編寫一個虛擬函數完成工作,然后再寫衣一個非虛擬函數,它什么也不做只是調用這個函數,可以使用內聯來避免函數調用的開銷。
條款26:限制某個類所能產生的對象數量
只 有一個對象:使用單一模式,將類的構造函數聲明為private,再聲明一個靜態函數,該函數中有一個類的靜態對象。不將該靜態對象放在類中原因是放在函 數中時,執行函數時才建立對象,并且對象初始化時間確定的,即第一次執行該函數時。另外,該函數不能聲明為內聯,如果內聯可能造成程序的靜態對象拷貝超過 一個。
限制對象個數:建立一個基類,構造函數中計數加一,若超過最大值則拋出異常;析構函數中計數減一。
編程點滴:
●將模板類的定義和實現放在一個文件中,否則將造成引用未定義錯誤(血的教訓);
●靜態數據成員需要先聲明再初始化;
●用常量值作初始化的有序類型的const靜態數據成員是一個常量表達式(可以作為數組定義的維數);
●構造函數中拋出異常,將導致靜態數組成員重新初始化。
條款27:要求或禁止在堆中產生對象
在堆中的對象不一定是用new分配的對象,例如成員對象,雖然不是用new分配的但是仍然在堆中。
要 求在堆中建立對象可以將析構函數聲明未private,再建立一個虛擬析構函數進行對象析構。此時如果建立非堆對象將導致析構函數不能通過編譯。當然也可 以將構造函數聲明為private,但是這樣將導致必須聲明n個構造函數(缺省,拷貝等等)。為了解決繼承問題,可以將其聲明為protected,解決 包容問題則只能將其聲明為指針。
沒有辦法不能判斷一個對象是否在堆中,但是可以判斷一個對象是否可以安全用delete刪除,只需在operator new中將其指針加入一個列表,然后根據此列表進行判斷。
把一個指針dynamic_cast成void*類型(或const void*或volatile void*等),生成的指針將指向“原指針指向對象內存”的開始處。但是dynamic_cast只能用于“指向至少具有一個虛擬函數的對象”的指針上。
禁止建立堆對象可以簡單的將operator new聲明為private,但是仍然不能判斷其是否在堆中。
條款28:靈巧(smart)指針
靈巧指針的用處是可以對操作進行封裝,同一用戶接口。
靈巧指針從模板生成,因為要與內建指針類似,必須是強類型的;模板參數確定指向對象的類型。
靈巧指針的拷貝和賦值,采取的方案是“當auto_ptr被拷貝和賦值時,對象所有權隨之被傳遞”。此時,通過傳值方式傳遞靈巧指針對象將導致不確定的后果,應該使用引用。
記住當返回類型是基類而返回對象實際上派生類對象時,不能傳遞對象,應該傳遞引用或指針,否則將產生對象切割。
測試靈巧指針是否為NULL有兩種方案:一種是使用類型轉換,將其轉換為void*,但是這樣將導致類型不安全,因為不同類型的靈巧指針之間將能夠互相比較;另一種是重載operator!,這種方案只能使用!ptr這種方式檢測。
最好不要提供轉換到內建指針的隱式類型轉換操作符,直接提供內建指針將破壞靈巧指針的“靈巧”特性。
靈巧指針的繼承類到基類的類型轉換的一個最佳解決方案是使用模板成員函數,這將使得內建指針所有可以轉換的類型也可以在靈巧指針中進行轉換。但是對于間接繼承的情況,必須用dynamic_cast指定其要轉換的類型是直接基類還是間接基類。
為了實現const靈巧指針,可以新建一個類,該類從非const靈巧指針繼承。這樣的化,const靈巧指針能做的,非const靈巧指針也能做,從而與標準形式相同。
條款29:引用計數
使用引用計數后,對象自己擁有自己,當沒有人再使用它時,它自己自動銷毀自己。因此,引用計數是個簡單的垃圾回收體系。
在基類中調用delete this將導致派生類的對象被銷毀。
寫時拷貝:與其它對象共享一個值直到寫操作時才擁有自己的拷貝。它是Lazy原則的特例。
精彩的類層次結構:
RCObject類提供計數操作;StringValue包含指向數據的指針并繼承RCObject的計數操作;RCPtr是一個靈巧指針,封裝了本屬于String的一些計數操作。
條款30:代理類
可以用兩個類來實現二維數組:Array1D是一個一維數組,而Array2D則是一個Array1D的一維數組。Array1D的實例扮演的是一個在概念上不存在的一維數組,它是一個代理類。
代 理類最神奇的功能是區分通過operator[]進行的是讀操作還是寫操作,它的思想是對于operator[]操作,返回的不是真正的對象,而是一個 proxy類,這個代理類記錄了對象的信息,將它作為賦值操作的目標時,proxy類扮演的是左值,用其它方式使用它,proxy類扮演的是右值。用賦值 操作符來實現左值操作,用隱式類型轉換來實現右值操作。
用proxy類區分operator[]作左值還是右值的局限性:要實現proxy類和原類型的無縫替代,必須申明原類型的一整套操作符;另外,使用proxy類還有隱式類型轉換的所有缺點。
編程點滴:不能將臨時對象綁定為非const的引用的行參。
條款31:讓函數根據一個以上的對象來決定怎么虛擬
有 三種方式:用虛函數加RTTI,在派生類的重載虛函數中使用if-else對傳進的不同類型參數執行不同的操作,這樣做幾乎放棄了封裝,每增加一個新的類 型時,必須更新每一個基于RTTI的if-else鏈以處理這個新的類型,因此程序本質上是沒有可維護性的;只使用虛函數,通過幾次單獨的虛函數調用,第 一次決定第一個對象的動態類型,第二次決定第二個對象動態類型,如此這般。然而,這種方法的缺陷仍然是:每個類必須知道它的所有同胞類,增加新類時,所有 代碼必須更新;模擬虛函數表,在類外建立一張模擬虛函數表,該表是類型和函數指針的映射,加入新類型是不須改動其它類代碼,只需在類外增加一個處理函數即 可。
條款32:在未來時態開發程序
未來時態的考慮只是簡單地增加了一些額外約束:
●提供完備的類,即使某些部分現在還沒有被使用。
●將接口設計得便于常見操作并防止常見錯誤。使得類容易正確使用而不易用錯。
●如果沒有限制不能通用化代碼,那么通用化它。
條款33:將非尾端類設計為抽象類
如果有一個實體類公有繼承自另一個實體類,應該將兩個類的繼承層次改為三個類的繼承層次,通過創造一個新的抽象類并將其它兩個實體類都從它繼承。因此,設計類層次的一般規則是:非尾端類應該是抽象類。在處理外來的類庫,可能不得不違反這個規則。
編程點滴:抽象類的派生類不能是抽象類;實現純虛函數一般不常見,但對純虛析構函數,它必須實現。
條款34:如何在同一程序中混合使用C++和C
混合編程的指導原則:
●確保C++和C編譯器產生兼容的obj文件。
●將在兩種語言下都使用的函數申明為extern ‘C’。
●只要可能,用C++寫main()。
●總用delete釋放new分配的內存;總用free釋放malloc分配的內存。
●將在兩種語言間傳遞的東西限制在用C編譯的數據結構的范圍內;這些結構的C++版本可以包含非虛成員函數。
條款35:讓自己習慣使用標準C++語言
STL 基于三個基本概念:包容器(container)、選擇子(iterator)和算法(algorithms)。包容器是被包容的對象的封裝;選擇子是類 指針的對象,讓你能如同使用指針操作內建類型的數組一樣操作STL的包容器;算法是對包容器進行處理的函數,并使用選擇子來實現。
文章來源:
http://my.donews.com/robinchow/2007/01/11/qimyhmnmnudhngtapaqrnsghpptjuutkcfjj/