More effective C++学习总结
More effective C++摘要
3 基礎議題部分:
3.1 M1:指針與引用的區別
首先,要認識到在任何情況下都不能使用指向空值的引用,引用必須被初始化。一個引用必須總是指向某些對象。
(不存在指向空值的引用意味著使用引用的代碼效率比使用指針的要高)
其次,指針可以被重新賦值以指向另一個不同的對象。
什么情況下應該使用指針??1存在不指向任何對象的可能。?2能夠在不同時刻指向不同對象
什么情況下應該使用引用??1總是指向一個對象并且不會改變指向?2重載某個操作符時
?
3.2 M2 盡量使用C++風格的類型轉換
為什么不使用C風格?1,過于粗魯,允許在任何類型之間進行轉換;?2,C風格的類型轉換在程序語句中難以識別
?C++新引進的風格
1 static_cast 在功能上基本上與C風格的類型轉換一樣強大,也有功能上的限制。但是不能從表達式中去除const屬性
2 const_cast 用于類型轉換掉表達式的const或者volatileness屬性。最普通的用途是轉換掉對象的const屬性
3 dynamic_cast 用于安全的沿著類的繼承關系向下進行類型轉換。即把指向基類的指針或引用轉換成指向其他派生類或者其兄弟類的指針或引用。失敗的轉換將返回空指針或者拋出異常
4 reinterpret_cast 轉換結果都是執行期定義,最普通的用途就是在函數指針類型之間進行轉換,但這種轉換是不可移植的
?
3.3 M3 不要對數組使用多態
通過一個基類指針來刪除一個含有派生類對象的數組,結果將是不確定的,指針和多態不能混在一起用,數組和多態也不能混在一起用。
BST BSTArray[10]; 數組中,參數array被聲明為BST類型,所以array數組中每一個元素都是BST類型,因此每個元素與數組起始地址的間隔是i*sizeof(BST)。但是如果把BalancedBST對象的數組變量傳遞給該數組,派生類的長度通常都比基類要長,數組定位指針算法將是錯誤的。
3.4 M4 避免無用的缺省構造函數
提供無意義的缺省構造函數也會影響類的工作效率。使用這種(沒有缺省構造函數的)類的確有一些限制,但是當你使用它時,它也給你提供了一種保證:你能相信這個類被正確地建立和高效地實現。
?
4 運算符部分:
4.1 M5 謹慎定義類型轉換函數
兩種函數允許編譯器進行隱式轉換:單參數構造函數和隱式類型轉換運算符。
1,什么是單參數構造函數?
單參數構造函數可以是只定義了一個參數,也可以是雖然定義了多個參數但第一個參數以后的所有參數都有缺省值。
2, 隱式類型轉換運算符只是一個樣子奇怪的成員函數:operator,其后跟一個類型符號。你不用定義函數的返回類型,因為返回類型就是這個函數的名字。
3,隱式類型可以轉換為顯式轉換函數(asDouble),雖然不方便,可以避免錯誤。即通過不聲明運算符operator的方法。
4,對于單參數構造函數,可以利用最新編譯器的特性,explicit關鍵字。構造函數如果用explicit聲明,編譯器會拒絕為了隱式類型轉換而調用構造函數
??
4.2M6自增(++)自減(--)操作符前綴與后綴形式的區別
1C++規定后綴形式有一個int類型參數,當函數被調用時,編譯器傳遞一個0作為int參數的值
? UPInt& operator++();?????????????????? // ++ 前綴
? const UPInt operator++(int);????????????? // ++ 后綴?
所以i++++錯誤,不允許,得不到正確的結果。所以,前綴的形式效率高,多使用。
2這些操作符前綴與后綴返回值類型不同。前綴形式返回一個引用,后綴形式返回一個const類型。因為:前綴形式有時叫做“增加然后取回”,操作的是當前的數據,故&;后綴形式叫做“取回然后增加”,操作的不是當前數據,故const。
?
4.3 M7不要重載”&&”和”||”和 “,”
1, &&和||是短路求值的:如果重載了這些,那么你以“函數調用”法替代了短路求值法,與C++規范不一致了
if (expression1 && expression2) ...等于
if (expression1.operator&&(expression2)) ...或者
if (operator&&(expression1, expression2)) ...。??不是短路操作。
2,逗號操作符,一個包含逗號的表達式首先計算逗號左邊的表達式,然后計算逗號右邊的表達式;整個表達式的結果是逗號右邊表達式的值。
?
4.4 M8理解各種不同含義的new和delete
共有三種不同形式的new
1 new 操作符 new operator??? string *ps = new string(“Memory Management”)
?第一 分配足夠的內存以便容納所需類型的對象
?第二 調用構造函數初始化內存中的對象
?
2 new操作 operator new 是new操作符為分配內存所調用函數的名字?
?所以operator函數只是用來分配內存,而且對構造函數一無所知。
void *operator new (size_t size) ,void *ramMemory = operator new(sizeof(string));
?
3 placement new
?特殊的operator new,,在已經被分配但是尚未處理的內存構造一個對象
?Placement new返回指向該內存的指針,因為operator new 的目的是為對象分配內存然后返回指向該內存的指針。
?Void *operatore new(size_t size,void *location){ Return location;}
new(buffer) Widget(widgetSize)
?
4 如何選取使用這三種不同類型的new
1) 如果想在堆上建立一個對象,選用new操作符。既分配內存又為對象調用構造函數
2) 如果僅僅分配內存,就調用operator new函數,不會調用構造函數
3) 如果在已經獲得指針的內存里建立一個對象,應該用placement new
Delete:operator delete與delete operator,?????? Operator new 與new operator
Delete ps:調用析構函數并釋放對象占有的內存
Operator delete(buffer)僅僅釋放資源到內存
?
5異常部分:
如果一個函數通過設置一個狀態變量或返回錯誤代碼來表示一個異常狀態,沒有辦法保證函數調用者將一定檢測變量或測試錯誤代碼。結果程序會從它遇到的異常狀態繼續運行,異常沒有被捕獲,程序立即會終止執行。
5.1 M9使用析構函數防止資源泄漏
使用auto_ptr,在構造函數里面,讓一個指針指向一個堆對象或者需要釋放的系統資源,并在析構函數里刪除這個對象。
隱藏在auto_ptr后的思想是:用一個對象存儲需要被自動釋放的資源,然后依靠對象的析構函數來釋放資源。
template <class T>
class auto_prt{
public:? auto_ptr(T *p=0): ptr(p) {}
?~auto_ptr(){delete ptr;}
private:?T *ptr;};
?
5.2M10在構造函數中防止資源泄漏
C++只能刪除被完全構造的對象,只有對象的構造函數完全運行完畢,這個對象才能被完全的構造;所以構造函數中出現泄漏的話,析構函數不能被調用。在構造函數中,處理各種拋出異常的可能,采用auto_ptr,可以化繁為簡
Auto_ptr特點:指向動態分配的對象的指針,當指針消失的時候,對象也應該被刪除
5.3M11禁止異常信息傳遞到析構函數
1,如果在一個異常被激活的同時,析構函數也拋出異常,并導致程序控制權轉移到析構函數外,C++將調用terminate函數。這個函數的作用正如其名字所表示的:它終止你程序的運行,而且是立即終止,甚至連局部對象都沒有被釋放。
所以,這樣做:能夠在異常轉遞的堆棧輾轉開解(stack-unwinding)過程中,防止terminate被調用。
2,如果一個異常被析構函數拋出而沒有在函數內部捕獲住,那么析構函數就不會完全運行(它會停在拋出異常的那個地方上)。如果析構函數不完全運行,它就無法確保析構函數總能完成我們希望它做的事情
?
5.4 M12 理解“拋出一個異常”與“傳遞一個參數”或“調用一個虛函數”間的差異
相同點:可以傳值,傳遞引用或者傳遞指針;造成程序流程改變并返回一些狀態;
不同點:1函數調用,程序的控制權最終還會返回到函數的調用處;拋出異常,控制權永遠不會回到拋出異常的地方。
2拋出異常的速度比參數傳遞慢。不論通過傳值或傳遞引用(指針不是),都將進行強制異常對象copy操作,因為異常導致了生命周期結束,C++規范要求被做為異常拋出的對象必須被復制。當異常對象被copy時,拷貝操作是對象的靜態類型所對應的拷貝構造函數,而不是對象的動態類型。
所以:異常對象在傳遞時基本上被進行拷貝;當通過傳值方式捕獲時,異常對象被拷貝了兩次。對象做為參數傳遞給函數時不一定需要被拷貝。
3異常拋出比參數傳遞轉換類型要少,支持的類型轉化少,僅支持兩種類型:1)繼承類與基類的轉換 2)允許從一個類型化指針轉變成無類型指針,所以帶有const void* 指針的catch子句能捕獲任何類型的指針類型異常.
4 catch子句進行異常類型匹配的順序即為他們在代碼中出現的順序,第一個類型匹配成功的catch將用來被執行。當一個對象調用一個虛擬函數時,被選擇的函數位于與對象類型匹配最佳的類里,即使該類不是在源代碼的最前頭。
5(zhs)如果一個函數通過設置一個狀態變量或返回錯誤代碼來表示一個異常狀態,沒有辦法保證函數調用者將一定檢測變量或測試錯誤代碼。結果程序會從它遇到的異常狀態繼續運行,異常沒有被捕獲,程序立即會終止執行。
?
5.5 Item13通過引用(reference)捕獲異常
通過指針捕獲異常雖說不能引起對象copy,效率高,但是不符合C++語言本身的規范。四個標準的異常――bad_alloc(當operator new不能分配足夠的內存時,被拋出),bad_cast(當dynamic_cast針對一個引用操作失敗時,被拋出),bad_typeid(當dynamic_cast對空指針進行操作時,被拋出)和bad_exception(用于unexpected異常)――都不是指向對象的指針,所以你必須通過值或引用來捕獲它們。
因為通過使用引用捕獲異常,可以避開(類似于指針那樣)異常對象是否已經被刪除而煩惱,能夠避開(類似于傳值那樣)slicing異常對象,減少異常對象需要被拷貝的數目。
?
5.6Item 14 謹慎使用異常規格(exception specification)
異常規格明確地描述一個函數可以拋出什么樣的異常 void f2() throw(int),但是實際上,不拋出int異常也是允許的。
5.7Item15了解異常處理的開銷
與一個正常的函數返回相比,通過拋出異常從函數里返回可能會慢三個數量級
?
6效率
6.1 Item M16 牢記80-20準則
80─20準則說的是大約20%的代碼使用了80%的程序資源,即軟件整體的性能取決于代碼組成中的一小部分。使用profiler來確定程序中的那20%,關注那些局部效率能夠被極大提高的地方。
6.2 Item M17 考慮使用LAZY EVALUATION(懶惰計算法)
采用此種方法的類將推遲計算工作直到系統需要這些計算的結果。
1,引用計數:除非確實需要,不去為任何東西制作拷貝。比如Copy-on-write。
2,區分對待讀取與寫入:operator[]因為讀取很容易,然而寫入這個string則需要在寫入前做一個copy,可以用代理類來實現。??3,懶惰存取:Lazy Fetch 每次不從磁盤上讀取所有數據。??4,懶惰表達式計算
?
6.3 Item M18 分期攤還期望的計算
過度熱情計算法(over-eager evaluation),如果一個計算需要頻繁進行,可以設計一個數據結構高效的處理這些計算需求。
6.4 Item M19 理解臨時對象的來源
這里的臨時對象是看不見的,不出現在源代碼中。通常兩種條件下會產生:
1為了使函數成功調用而進行的隱式類型轉換。在任何時候只要見到常量引用(reference to const)參數,就存在建立臨時對象而綁定在參數上的可能性。僅當通過傳值方式傳遞對象或傳遞常量引用參數時,才會可能發生這些類型轉換。當傳遞一個非常量引用參數對象,就不會發生:報錯。
當程序員期望修改非臨時對象時,對非常量引用(references-to-non-const)進行的隱式類型轉換卻修改臨時對象。這就是為什么C++語言禁止為非常量引用(reference-to-non-const)產生臨時對象。
2 函數返回對象時。在任何時候只要見到函數返回對象,就會有一個臨時對象被建立。以某種方法返回對象,但不讓編譯器創建臨時對象,這種技巧是返回constructor argument而不是直接返回對象(這叫:返回值優化):
return Rational(x,y);
?
6.5 Item M20 協助完成返回值優化
消除傳值返回的對象的努力不會獲得勝利,所以我們只能減小對象開銷
1直接返回constructor argument而不出現局部對象
2通過聲明函數為inline來消除operator調用開銷
6.6 Item M21通過重載避免隱式類型轉換
因為編譯器完成這種隱式轉換是有開銷的,所以希望避免這種隱式的轉換。
在C++中有一條規則是每一個重載的operator必須帶有一個用戶定義類型(user-defined type)的參數。
?
6.7 Item M22 考慮用運算符的賦值形式(op=)取代其單獨形式(op)
這是因為賦值形式的效率要高:
1 operator的賦值形式比單獨形式效率更高,因為單獨形式要返回一個新對象,存在構造和釋放的開銷
2提供operator賦值形式的同時也要提供其標準形式,允許類的客戶端選擇
3不能在operator+里使用返回值優化
6.8 Item M23 考慮變更程序庫
具有相同功能的不同程序庫在性能上采取不同的權衡措施,所以一旦找到瓶頸,可考慮通過變更程序庫提高效率
?
6.9 Item M24 理解虛擬函數,多繼承,
虛擬函數,大多數編譯器的實現是通過virtual table和virtual table pointers,分別被稱為vtbl和vptr
vtbl是一個函數指針數組,程序中的每個類只要聲明了虛函數或繼承了虛函數,就有自己的vtbl,vtbl的項目是指向虛函數實現體的指針
vptr:每個聲明了虛函數的對象都帶有vptr,它是一個看不見的數據成員,指向對應類的virtual table
?
所以虛擬函數代價:
1必須為每個包含虛函數的類的virtual table留出空間(每個類有一個虛表), vtbl的大小與類中聲明的虛函數的數量成正比(包括從基類繼承的虛函數).?
采用啟發式算法來決定哪一個object文件應該包含類的vtbl:要在一個object文件中生成一個類的vtbl,要求該object文件包含該類的第一個非內聯、非純虛擬函數。所以避免把虛函數聲明為內聯函數。
2在每個包含虛函數的對象里,必須為額外的指針付出代價
3放棄使用了內聯函數: “內聯”是指“在編譯期間用被調用的函數體本身來代替函數調用的指令,”但是虛函數的“虛”是指“直到運行時才能知道要調用的是哪一個函數。”如果編譯器在某個函數的調用點不知道具體是哪個函數被調用,你就能知道為什么它不會內聯該函數的調用。所以內聯和虛擬是互相矛盾的。(當通過對象調用虛函數時,它可以被內聯,但是大多數虛函數是通過對象的指針或引用被調用的,需要函數生成實體和地址,所以這種調用不能被內聯。因為這種調用是標準的調用方式,所以虛函數實際上不能被內聯。)
通過指針pC1調用虛擬函數f1,編譯器生成的代碼會做如下這些事情:
1)通過對象的vptr找到類的vtbl,因此這個代價只是一個偏移調整(以得到vptr)和一個指針的間接尋址(以得到vtbl)。
2)找到對應vtbl內的指向被調用函數的指針,這步的代價只是在vtbl數組內的一個偏移。
3)調用第二步找到的的指針所指向的函數。
如果我們假設每個對象有一個隱藏的數據叫做vptr,而且f1在vtbl中的索引為i,此語句
pC1->f1();?生成的代碼基本就是這樣的?(*pC1->vptr[i])(pC1);??
這幾乎與調用非虛函數效率一樣,調用虛函數所需的代價基本上與通過函數指針調用函數一樣,本身不是性能的瓶頸。
多繼承:
在多繼承里,單個對象里有多個vptr(每個基類對應一個),為尋找vptr而進行的計算更為復雜
?
RTTI:和多態相關,因為運行時找到對象和類的相關信息,所以type_info對象存儲了這些,通過typeid操作符訪問類的type_info對象。語言規范上這樣描述:我們保證可以獲得一個對象動態類型信息,如果該類型有至少一個虛函數。所以,RTTI被設計為在類的vtbl基礎上實現。使用這種實現方法,RTTI耗費的空間是在每個類的vtbl中的占用的額外單元再加上存儲type_info對象的空間。
?
7 技巧
7.1 Item M25將構造函數和非成員函數虛擬化
虛擬構造函數是指能夠根據輸入給它的數據的不同而建立不同類型的對象,然而實際上它并不是真正的構造函數:真正的構造函數不可能做成虛擬的。虛擬拷貝構造函數只是調用它們真正的構造函數。
虛擬非成員函數的實現,是通過:public接口(可以內聯) + private虛擬函數。非成員函數調用public接口。
7.2 Item M26 限制某個類所能產生的對象數量
(zhs)這里有一些理論不敢茍同,可以參照singleton的方法進行。
7.3 Item M27 要求或禁止在堆中產生對象
1)要求在堆中建立對象:(因為非堆對象在定義它的地方自動構造,在生存時間結束時自動被釋放,因此禁止使用隱式的構造函數和析構函數就可以實現)。構造函數設為public,析構函數設為protected(既可以派生,還禁止了外部釋放)。
2)判斷一個對象是否在堆中:可以轉化為判斷是否能夠刪除一個指針,將new中得到的地址加入list中,通過查找可判斷是否能刪除指針。
3)把一個指針dynamic_cast成void*類型(或const void*或volatile void*等),生成的指針將指向“原指針指向對象內存”的開始處。但是dynamic_cast只能用于“指向至少具有一個虛擬函數的對象”的指針上。
上面是要求在堆上,下面是禁止在堆上。
“禁止在堆中建立對象”,三種情況:對象被直接實例化;對象做為派生類的基類被實例化;對象被嵌入到其它對象內。
1)聲明operator new函數,而且為private,不但該類,連同派生類也被禁止了在堆中創建;
2)禁止堆對象 判斷如果在堆中,則拋出一個異常
7.4 Item M28 智能指針
1, 1)智能指針從模板中生成,因為要與內建指針類似,必須是strongly type(強類型)的。
2)如ECpp所述,當auto_ptr被拷貝和賦值時,管理的對象可以有多種處理;如果對象所有權被傳遞,則函數要使用傳引用而不是傳值
3)實現*返回類型必須是引用:a)如果返回了一個基類對象而不是一個派生類對象的引用,會導致slicing問題; b)避免生成新的臨時對象
4) 實現 -> 返回類型是內部對象的指針,或者是另外的一個智能指針
2,測試智能指針是否為NULL:共2種方法:
1)提供隱式類型操作符,用于這種目的類型轉換的是void *:operator void*();
2)在智能指針中重載operator!,當且僅當智能指針是一個空指針時,operator!返回true.
3,除非有讓人信服的原因,否則絕對不要提供轉換到dumb指針的隱式類型轉換操作符。
4,智能指針和繼承類到基類的類型轉換: SmartPtr<Cassette>向SmartPtr<MusicProduct>轉換。在繼承類向基類進行類型轉換方面,我們如何能夠讓智能指針的行為與dumb指針一樣呢?答案很簡單:不可能。
5,智能指針和const
const SmartPtr<const CD> p = &goodCD;??? // const 對象,const 指針
使用union支持const和非const兩個指針,但是又不增加額外空間。
union { const T* constPointee;?????????? // 讓 SmartPtrToConst 訪問
???? T* pointee;????????????????????? // 讓 SmartPtr 訪問? };
?
7.5 Item M29 引用計數
1,引用計數是基于對象通常共享相同的值假設的優化技巧。目的:a) 簡化跟蹤堆中的對象的過程,是個簡單的垃圾回收體系;b) 節省內存,提供性能(因為不需要構造和析構這個值的拷貝)
2,寫時copy(copy on write):與其他對象共享一個值直到寫操作時才擁有自己的copy.Lazy原則特例。保守地假設“所有”調用非const operator[]的行為都是為了寫操作;當然所有const版本都是讀
3,使用帶有引用計數的基類,在內部使用智能指針來控制計數的增加和減少;
?
7.6 Item M30 代理類
代理類通常扮演其它對象的對象,通過代理類可以實現
1二維數組,Array1D扮演一維數組,是代理類。怎么實現?
2區分通過operator[]進行的是讀操作還是寫操作。通過修改operator[]讓他返回一個proxy對象而不是字符本身
如果對左值來說,編譯器努力的尋找一個隱式類型轉換
如果對右值來說,賦值操作被調用
??? CharProxy& operator=(const CharProxy& rhs);???? // lvalue: s1[3] = s2[8]
??? CharProxy& operator=(char c);????????????????? // lvalue: s2[5] = 'x'
??? operator char() const;???????????????????????? // rvalue:cout << s1[5]
3限制了隱式類型轉換
7.7 Item M31 讓函數根據一個以上的對象來決定怎么虛擬
0)如果你需要實現二重調度,最好的辦法是修改設計以取消這個需要。
1)一個操作,取決于兩個對象的動態類型,使用一個對象的虛擬函數還不夠。…
?
8雜項
8.1 Item M32 在未來時態下開發程序
提供完備的類,即使某些部分現在還沒有被使用
將接口設計得便于常見操作并防止常見錯誤(E46),阻止拷貝構造和賦值操作如果類不需要(E27),防止部分賦值(M33)
8.2Item M33 將非尾端類設計為抽象類
8.3Item M34 如何在同一程序中混合使用C++和C
名變換:C++編譯器給每個函數換一個獨一無二的名字,因為C++引入了函數重載,但是C并不需要函數重載,如果要在C++里使用C函數,就要禁止名變換,使用C++的extern “C”指示: extern "C" void simulate(int iterations);
Extern “C”可以對一組函數生效: extern "C" {…}
靜態初始化:在main執行前后都有大量的代碼被執行,尤其是靜態的類對象和定義在全局的、命名空間中的或文件中的類對象的構造函數通常在main被執行前調用(E47)。對應的析構函數在main結束運行之后。如果main不是C++寫的,那么這些對象從來沒對初始化和析構.
C99 標準中,只有以下兩種定義方式是正確的: int main( void ); 和int main( int argc, char *argv[] );
C++98 中,如下兩種 main 函數的定義方式:? int main( );??? 和int main( int argc, char *argv[] );?
動態內存分配:總用delete釋放new分配的內存,總用free釋放malloc分配的內存
數據結構的兼容性:在C++和C之間相互傳遞數據結構是安全的,C++和C提供同樣的編譯。C++除了增加非虛成員函數不影響兼容性外,都影響兼容:虛函數,繼承有基類的內容
?
8.4 Item M35 讓自己習慣使用標準的C++語言
標準C++中STL中有三個概念:1容器(Container) 2迭代器(iterator)3算法(algorithm)
參考:http://www.diybl.com/course/3_program/c++/cppsl/200819/96109_2.html
轉載于:https://my.oschina.net/u/256892/blog/371060
總結
以上是生活随笔為你收集整理的More effective C++学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第一次有人把 5G 讲的这么简单明了
- 下一篇: 自学Python第十四天- 一些有用的模