Exceptional C++ 讀書筆記
?Item 1 Iterator:
???
?(1): 注意當前迭代器是否有效,如果無效則解引用產生程序錯誤;
?(2):注意當前迭代器生命期,某些容器經過某些操作后將重新分配內部存儲空間,則當前迭代器無效;
?(3) : 有效范圍, 類似find(first, last, value)時, 迭代器first 必須在last之前,必須保證指向同一個容器;
?????? (4): 有效的內部操作;
?Item 2, 3: Case-Insensitive String
?????
?(1) 1 關于std::string: std::string實際是
??template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT> >
??class basic_string;
??typedef basic_string<char> string;
??要實現case-insensitive string需要改變string的compare方式,因為char_traits<char>類提供compare等方式,所以
??就需要對char_traits<char>作相應的變動;
??char_traits::eq()和lt()提供相等和小于的對比方式,compare()和find()提供對比和搜索字符串序列的功能;更改這四個成員函數??就可以實現case-insensitive string;
?(2)關于實際應用中,最好選用一個單一的函數去接受兩個相應的std::string對象,而非用更改traits的方法來實現一個ci_string;
?例如 bool CaseInsensitiveString(std::string &a, std::string &b)
?(3)類似ci_char_traits : public char_traits<char>,
用于變換traits或者iterator功能等情形,而非當作傳統的OO方式來使用時,可以當作合理的派生,不必考慮virtual
destructor等;
?
?Item 4, 5 Max Reusable Generic Containers
?(1) template constructor永遠不會是一個copy constructor, 因為永遠存在一個最適合的copy
constructor, 因此overload resolution決不會將template constructor 當作copy
constructor
?
?(2) 善于使用模板成員函數
?Item 6, 7 Temporary Objects
?
?(1) a: 用傳遞const T& 代替傳值;
???????? b: 用預先創建的對象代替不需要的重復創建的對象,類似(container.end()每次被調用將返回一個臨時的對象,這是不必要的);
???? c: 用++T 代替 T++ (因為T++ 一般先制造一個臨時對象,然后更新本身,最后返回臨時對象);
???????? d: 用++T來實現T++;
???? e: 避免做無用的隱式轉換;
?
?(2) 注意對象生命期,絕不準讓函數返回一個函數內部的auto變量的指針或者引用;
?(3) 多用標準庫的代碼;
?Item 8 -- 17 Writing Exception-Safe Code
?(1)當一個函數不能處理內部的異常時,應當將它拋給函數調用者去處理;
?(2) 當異常出現時,請讓資源正確的被釋放,數據處于一個穩定的狀態;
?(3)絕不能讓從destructor或者被重載的delete中拋出,寫析構函數和資源分配函數時應當寫上throw();?
?(4)先將所有可能拋出異常的操作和安全的的操作分離出來,當可能拋出異常的操作執行成功時,再用no throw操作改變對象狀態;
?(5)讓每一個代碼片段(類,模塊,函數)負責單一的任務,類似STL stack中的pop和top是分離的
?
?(6): exception-safe基本準則:
??A: 基本安全: 當異常拋出時不產生資源泄漏;
??B: 健壯的安全性:如果某個操作因為異常的拋出而終止,則對象當前狀態不應改變;
??C: 不拋出異常: 約定這個函數決不拋出異常;
?
?(7): 將資源管理單獨的封裝成一個類類似書上的例子 StackImpl封裝了內存管理功能
?(8): 多用聚合,少用繼承(包括這種聚合 pivate繼承 -- 因為這將導致在constructor中無法控制base class
?(9): 用資源申請既初始化來隔離類的實現和資源的管理與所有權
?
?(10): 什么時候應當用private繼承代替containment?
??a: 需要存取某個類的protected成員
??b: 需要覆寫虛函數
??c: base 對象需要在其他類對象建構之前建構
?(11): 析構函數中決不允許拋出異常:
??a: 對象本身所申請的的資源不能完全釋放;
??b:考慮 T t[20];的情況;析構某個T時拋出異常會產生未定義情況
??
Item 18, 19 Code Complexity
?(1): 提供強異常安全需要損失一定程度的性能;
?(2): 如果一個函數存在幾個不相關的副作用,那么它不可能實現異常安全, 否則應當把這幾個函數分割為不同的函數
?(3): 不是所有的函數都需要強異常安全;
?Item 20 Class Mechanics
?(1):?設計時首先考慮標準庫是否存在相關的類
?(2): ?用explicit constructor代替隱式轉換
?(3): 如果提供一個常規的operator op(類似+),那么應該同樣提供一個operator op=(+=),
??operator op(類似的 + - * /)不應該改變對象值, 它應當返回一個 + 操作后的臨時對象;
?(4): 多用a op= b 代替 a = a op b; 一般 const T operator op(T, T)產生一個臨時變量;
?
?(5) ?一元運算符是成員函數
?????? =,(),[]和->必須是成員函數
?????? +=,-=,/=,*=(等等)是成員函數
?????? 所有其它的二元運算符是非成員函數
?(6) ++T 應當返回一個引用, T++應當返回一個const T,以防止 T++++這種情況發生;T++應當由++T來實現;
?(7) 避免使用前導下劃線來命名,前導下劃線是留給編譯器使用的
?
?Item 21 override 虛函數?
?(1): 除非確定某個類不被派生,否則都應當提供一個虛函數;
?(2): 關于 overload, override和 hide
??a: overload意思是提供一個相同名稱但是不同參數的函數, 編譯時由編譯器決定哪個是最匹配的函數
??b: override意思是在派生類提供一個相同名稱且相同參數不同實現的虛函數,動態調用時將從派生類指針調用這個函數
??c: hide 某個函數的意思是有一個函數在某個范圍內(派生類,嵌套類,名稱空間等),隱藏同名函數
??
?(3) 當派生類提供一個和基類的某個成員函數同名的函數時,如果你不想隱藏基類的同名函數,請在派生類聲明基類的同名函數,
??例如: using Base::f;
?(4) 永遠不要在派生類override virtual function中改變默認參數;
?
?Item 22, 23 Class Relationships
?(1): 什么時候不應該選用public 繼承
????? a: 如果某個類為提供虛函數,這個類本身不打算實施多態;
????? b: 如果某個類未提供protected 成員
????? c: 類型為不是IS-A, WORKS-LIKE-A or USABLE-AS-A;
????? d: 派生類只用了基類的public 成員;
?(2): 只有當需要存取某個類protected 成員或者override 虛函數的時候才應該選用private繼承;不是IS-A, WORKS-LIKE-A or
????? USABLE-AS-A的時候決不應當用public 繼承;
?
?(3): 避免提供public virtual function, 使用template method 設計模式代替;從算法中提取出那些每次都要進行的步驟,將其抽象出??來,只把一些因地制宜的細節通過virtual function 留給派生類來實現。
?(4): 應用compiler-firewall 方法隱藏實現細節,使用一個不透明的指針(指向一個聲明但未定義的類
??(struct xxxImpl; xxxImpl *pimpl_)來存private 成員,包括狀態變量和成員函數);
??例如: clas Map { private: struct MapImpl; MapImpl *pimpl_; };
?
?
?Item 24 Uses and Abuses of Inheritance
?(1): 多用聚合,(containment, composition, layering HAS-A, delegation),當關系模型為 IS-IMPLEMENTED-IN-TERMS-OF時,請多考慮聚合,最好不用繼承;
?(2): 非public 繼承的條件:
??a: 當需要override virtual function時候;
??b: 需要存取基類protected成員時候
??c: 需要保證初始化順序的時候(基類必須建構在派生類之前,析構在派生類之后等情況);
??d: 需要某種多態的訪問控制時;
?(3): 用public繼承時應當避免的問題:
??a: 如果非public繼承可以做,那么決不要用public繼承;絕不能用public繼承表述一個依據什么實現的關系模型;
??b: 絕不能用public繼承表述一個 IS-ALMOST-A關系模型(類似正方形是一個矩形等關系模型);
?(4): 應當總是先確定 public繼承關系模型為 IS-A或者WORKS-LIKE-A
?Item 25 Object-Oriented Programming
?略;
?Item 26-28 Minimizing Compile-Time Dependencies
?(1): 決不#include無用的頭文件;
?(2): 當前向聲明某個stream時候用<iosfwd>足夠了;
?(3): 如果composition足夠的話,決不能用繼承;
?
?Item 29 Compilation Firewalls
?(1): 放置所有的非virtual函數 private member 到Pimpl類中去; 客戶程序員無論如何也不應當看到這些private成員;
????? Pimpl類中某些函數可能需要類中某些成員的支持,所有需要一個back pointer指向類本身,back pointer通常命名為self_
?
?Item 30 The "Fast Pimpl" Idiom
?(1):絕不能放置對象到一個buffer里面,例如
??char buffer[100];
??new (&buffer[0]) T;
??a: 不能保證對齊規則;
??b: 脆弱
??c: 難于維護;
??
?
?Item 31 - 34 Name Lookup and Interface Principle
?(1): 對于一個class X,任何提及并且含有X(例如來自同一頭文件或者名稱空間)的函數(括成員函數,static函數和自由函數),邏輯上都屬于class X的一部分,因為它們是X接口的一部分
??任何class X的成員函數都一定提及X;
??任何class X的成員函數都一定含有X(隱式的this);
??例如:
???class X{...};
???ostream& operator<<(ostream&, const X&)//肯定提及X;
???如果operator<<含有X ,例如在同一個頭文件或者名稱空間,那么它邏輯上屬于X的一部分,因為他建構了X的一部分接口;
???否則反之;
??
?(2): 關于 supplied with;
??struct _iobuf{...};
??typedef struct _iobuf FILE;
??FILE*?fopen(const char *filename, const char* mode);
??int fclose(FILE* stream);
??int fseek(FILE* stream, long offset, int origin);
??......
??這些函數都是非成員,但是它們確實是 FILE接口的一部分;(part of the interface of FILE);
??class File
??{
??public:
???int fseek(long offset, int origin);
???......
??};
??int fseek(FILE* stream, long offset, int origin);
??例如上例:?int fseek(FILE* stream, long offset, int origin);
??提及并且同File都來自于頭一個頭文件或者名稱空間,此函數一樣屬于File接口的一部分;
?
?(3):如果在某個作用域內調用了一個接受另一個作用域內類型參數的函數,那么編譯器不止在當前作擁域內查找該函數,同時也會到接受的的??類型所在的作用域去查找該函數,而且不用特別聲明;類似
??void f()
??{
???std:string hello = "hello world";
???std::cout? <<? hello;?? //正確調用std::operator<<;
??}
??
如果沒有Koenig
lookup,則編譯器無法查找到operator<<是哪一個我們想要的;(這個函數封裝在std::string內,是string接口
的??一部分);如果希望正確調用的operator<<,我們需要 std::operator<<(std::cout,
hello);
??
??所以,如果在同一個名稱空間內定義了一個類和一個提及這個類的函數, 那么編譯器將強制關聯這個類和這個自由函數;
?(4): namespace A { class X {};? void f(X x) {} }
??namespace B { void f(A::X x) {} void g(A:: xx) { f(xx); } }
??這將導致B::g出現二義性而無法編譯;原因在于g涉及A::X,所以編譯時編譯器同樣將名稱空間A內的函數也考慮進來,此時出現兩個參??數為A::X的函數f
??類的成員函數將被優先考慮,例如
??namespace A { class X {};? void f(X x) {} }
??class B
??{
???void f(A::X x) {}
???void g(A:: xx) { f(xx); } //此時不會出現二義性問題;
??};
??如果編譯器發現一個類成員函數為f(A::X),則不再使用Koenig lookup去搜索自由函數;
??
?(5):結論為名稱空間不像想象的那樣完全隔離函數聲明,它只能屬于部分獨立;
?
?
?(6): 根據Interface Principle,如果一個函數提及了兩個類,class X和class Y, 并且此函數同X包含在同一個名稱空間或者頭文件之中,那么,首先,此函數邏輯上是X的界面接口的一部分;由于此函數依賴Y,因此,X同樣依賴于Y;
?(7): 如果A,B是類,f(A,B)是自由函數, 那么
??a: 如果A和f在同一頭文件或名稱空間內,那么f邏輯上是A的一部分,所以,A也依賴于B;
??b: 如果A,B和f在同一頭文件或名稱空間內,那么f邏輯上是A的一部分也是B的一部分,故此,A,B互相依賴;
?
??如果 ,A,B是類,A::g(B)是一個A的成員函數,那么:
??a: 因為A::g(B),所以,A,依賴于B;
??b:如果A,B被聲明在一起(同一個頭文件或名稱空間),那么A,B互相依賴,A::g(B)邏輯上同樣屬于B的一部分;
??實際上,上述情況的實際含義是,類型A,B通常將被放在一起使用,,如果修改了一個,那么將影響另一個;
?(8):關于名稱隱藏:
??struct B
??{
???int f(int);
???int f(double);
???int g(int);
??};
??struct D : public B
??{
??private:
???int g(std::string, bool);
??};
??D d;
??int i;
??d.f(i) ?//調用B::f(int);
??d.g(i);?//錯誤,提示應當提供兩個參數;
??
???
實際上,編譯器查找匹配函數名稱時候,先查找當前范圍內的名稱匹配函數,例如這里是D,然后列出一個編譯器所能找到的函??數名稱?相同的函數的列表,但
是并不考慮存取權限和所攜帶參數是否正確,如果它一個名稱匹配的函數都沒找到的話,那么將向外一層??繼續查找,如果編譯器找到一個或者多個候選的函數,
編譯器將停止查找,然后提交函數的重載決議;
??兩種解決方法是
??a: d.B::g(i);//編譯器自然會考慮B的范圍;
??b:
???struct D : public B
???{
???using B::g;
???private:
????int g(std::string, bool);
???};
??
??
??
?(9): 使用名稱空間,如果寫一個類在名稱空間N中,那么請將所有的協助函數和操作符都都寫在N中;
Item 35, 36 Memory Management
?
?(1):
??[常量數據(const data)區:]
??常量數據區存儲字符串等在編譯期間就能確定的值。類對象不能存在于這個區域中。在程
序的整個生存周期內,區域中的數據都是可??用的。區域內所有的數據都是只讀的,任何企圖修改本區域數據的行為都會造成無法預料的后果。之所以會如此,是
因為在實際的實現??當中,即使是最底層的內部存儲格式也受制于所實現的特定的優化方案。例如,一種編譯器完全可以把字符串存放在幾個重疊的對象里??面
——只要實現者愿意的話。?
??[棧(stack)區:]
??棧區存儲自動變量(automatic
variables)。一般來說,棧區的分配操作要比動態存儲區(比如堆(heap)或者自由存儲區(free
??store))快得多,這是因為棧區的分配只涉及到一個指針的遞增,而動態存儲區的分配涉及到較為復雜的管理機制。棧區中,內存一??旦被分配,對象
就立即被構造好了;對象一旦被銷毀,分配的內存也立即被收回(譯注:這里作者用了“去配(deallocate)”一詞,??鄙人一律翻譯為“回收”)。
因此,在棧區中,程序員沒有辦法直接操縱那些已經被分配但還沒有被初始化的棧空間(當然,那些通過??使用顯式(explicit)析構函數
(destructor)和new運算符而故意這么做的情況不算在內)。
??[自由存儲區(free store):]
? ??自由存儲區(free
store)是C++兩個動態內存區域之一,使用new和delete來予以分配和釋放(freed)。在自由存儲區(free
???store)中,對象的生存周期可以比存放它的內存區的生存周期短;這也就是說,我們可以獲得一片內存區而不用馬上對其進行初始??化;同時,在對
象被銷毀之后,也不用馬上收回其占用的內存區。在對象被銷毀而其占用的內存區還未被收回的這段時間內,我們可以??通過void*型的指針訪問這片區域,
但是其原始對象的非靜態成員以及成員函數(即使我們知道了它們的地址)都不能被訪問或者操??縱。
??
??[堆(heap)區:]
??
堆(heap)區是另一個動態存儲區域,使用malloc、free以及一些相關變量來進行分配和回收。要注意,雖然在特定的編譯器里缺省的??全局運?
算符new和delete也許會按照malloc和free的方式來被實現,但是堆(heap)與自由存儲區(free
store)是不同的——在某??一個區域內被分配的內存不可能在另一個區域內被安全的回收。堆(heap)中被分配的內存一般用于存放在使用new的構
造過程中和顯??式(explicit)的析構過程中涉及到的類對象。堆中對象的生存周期與自由存儲區(free store)中的類似。
??[全局/靜態區(Global/Static):]
??
??全局的或靜態的變量和對象所占用的內存區域在程序啟動(startup)的時候才被分配,而且可能直到程序開始執行的時候才被初始??化。比如,函數
中的靜態變量就是在程序第一次執行到定義該變量的代碼時才被初始化的。對那些跨越了翻譯單元(translation
???unit)的全局變量進行初始化操作的順序是沒有被明確定義的,因而需要特別注意管理全局對象(包括靜態類對象)之間的依賴關系。??最后,和前面
講的一樣,全局/靜態區(Global/Static)中沒有被初始化的對象存儲區域可以通過void*來被訪問和操縱,但是只要是??在對象真正的生存
周期之外,非靜態成員和成員函數是無法被使用或者引用的。
??
??[關于“堆(heap)vs.自由存儲區(free store)”]:
??本條款中我們將堆(heap)和自由存儲區(free store)區分開來,是因為在C++標準草案中,關于這兩種區域是否有聯系的問題一直??很謹慎的沒有予以詳細說明。比如當內存在通過delete運算符進行回收時,18.4.1.1中說道:
???It
is unspecified under what conditions part or all of such reclaimed
storage is allocated by a ????subsequent call to operator new or any of
calloc, malloc, or realloc, declared in <cstdlib>.
???[關于在何種情況下,這種內存區域的部分或全部才會通過后續的對new(或者是在<cstdlib>里聲明的calloc,malloc,????realloc中的任何一個)的調用來被分配的問題,在此不作詳細規定,不予詳述。]
?
??????
同樣,在一個特定的實現中,到底new和delete是按照malloc和free來實現的,或者反過來malloc和free是按照new和
delete來實現??的,這也沒有定論。簡單地說吧,這兩種內存區域運作方式不一樣,訪問方式也不一樣——所以嘛,當然應該被當成不一樣的兩個東西??
來使用了!
?
?Item 37: AUTO_PTR
??略
?Item 38 Object Identity
?(1): 千萬別寫一個依賴于檢測自身賦值達到正確操作的operator=(),一個拷貝賦值操作符應當使用create-a-temporary-and-swap
??方法來自動維持強異常安全和安全的自身賦值;
?
?(2): 自身賦值是個可行的優化去避免無用的工作;
?
?Item 39 Automatic Conversions
?
?(1): 避免使用隱式轉換操作符:
??a: 隱式轉換會影響重載決議;
??b: 隱式轉換會讓錯誤的代碼安靜的編譯通過;
?(2):?C++標準保證「對指向同一個對象的多個指針的比較之結果必須是“相等(equal)” 」,但卻并不保證「對指向不同對象的多個指針??的比較之結果必須是“不相等(unequal)”」。
?Item 40,41 Object Lifetimes
?
?(1):?void f()
??{
???T t(1);
???T& rt = t;
???//#1 ?do something;
???t.~T();
???new (&t) T(2);
???//#2 ?do something;
??};
?a: #2是合法的;
?b: #2是不安全的;
?請總是嘗試寫異常安全級別的代碼, 總是嘗試正確的組織代碼讓資源正確的申請和釋放當異常拋出時;
?
?(2): 避免C++ 語言級別定義不太明確的技巧,應該多用清晰簡單的編程技巧;???
?(3): 用同一個成員函數完成兩種拷貝操作(拷貝構造和拷貝賦值)的注意是很好的:這意味著我們只需在一個地方編寫和維護操作代碼。本條??款問題中的慣用法只不過是選擇了錯誤的函數來做這件事。如此而已。
?
??其實,慣用法應該是反過來實現的:拷貝構造操作應該以拷貝賦值操作來實現,不是反過來實現。例如:
??T::T( const T& other ) {
??????? /* T:: */ operator=( other );
??}
??? ??
??T& T::operator=( const T& other ) {
?????? ?// 真正的工作在這里進行
????? ??// (大概可以在異常安全的狀態下完成,但現在
????? ??// 其可以拋出異常,卻不會像原來那樣產生什么不良影響
???? ?? ?return *this;
??? ??}
??這段代碼擁有原慣用法的所有益處,卻不存在任何原慣用法中存在的問題。[注5] 為了代碼的美觀,你或許還要編寫一個常見的私有輔??助函數,利用其做真正的工作;但這也是一樣的,無甚區別:
??? ??T::T( const T& other ) {
????? ??do_copy( other );
??? ??}
??? ??
??T& T::operator=( const T& other ) {
????? ??do_copy( other );
????? ??return *this;
??? ??}
??? ??
??T& T::do_copy( const T& other ) {
????? ??// 真正的工作在這里進行
????? ??// (大概可以在異常安全的狀態下完成,但現在
????? ??// 其可以拋出異常,卻不會像原來那樣產生什么不良影響
??}
?
?
??如果需要的話,請編寫一個私有函數來使拷貝操作和拷貝賦值共享代碼;千萬不要利用「使用顯式的析構函數并且后跟一個
placement
??new」的方法來達到「以拷貝構造操作實現拷貝賦值操作」這樣的目的,即使這個所謂的技巧會每隔三個月就在新聞組中出現幾次。??(也就是說,決不要
編寫如下的代碼:)
??????? ?T& T::operator=( const T& other )
??????? ?{
??????????? ?if( this != &other)
??????????? ?{
??????????????? ??this->~T();???????????? // 有害!
??????????????? ??new (this) T( other );? // 有害!
??????????? ?}
??????????? ?return *this;
??????? ?}
?
?Item 42: Variable Initialization -- Or is it?
?(1): 關于 T u; T t = u;
??這時,編譯器總會是調用 copy-construct而不是調用默認constructor之后調用operator=(),這只是一個從C繼承來的語法,???并沒有賦值操作
?(2): 用 T t(u);代替 T t = u;
?Item 43 Const-Correctiness;
?(1): 避免在某個函數參數使用 const by value
?(2): 如果是非內部類型的return by value,那么優先考慮返回一個const value;
?(3): 如果某個類數據成員的改變并不影響到類狀態的改變,例如class X { private: char buffer[1024]; ...};
??那么當某函數只改變這個類數據成員,而不改變其他成員時,也應當把這個函數為const ,并將這個被改變但無關類狀態的成員聲明為
??mutable;
?Item 44 Casts
?(1): 請優先使用新轉型風格cast;
?(2): 避免用const_cast消除const ,請優先使用mutable;
?(3): 避免動態的向下轉型;
?Item 45: BOOL
?略
?Item 46 Forwarding Functions;
?(1): 多考慮傳遞對象的const &;
?(2): 避免語言的隱晦角落,多用簡單的技巧;
?(3): 如果不認為性能上絕對需要,請避免inline或者內部優化;
?Item 47 Control Flow
?(1): 避免使用全局或者靜態變量;如果必須如此,那么請注意它們的初始化順序(因為多個頭文件內引入的靜態變量的聲明順尋并不是標準定??義的);
?(2): 將基類的constructor在初始化器列表中的順序和類聲明的順序是一樣的,基類的初始化順序是依據類定義時的順序的;
?(3): 將類的數據成員的初始化順序與初始化器列表中聲明的順序是一樣的,同樣依據類定義;
?(4): 努力去實現代碼的異常級安全;
?(5): 絕不能寫依賴函數參數賦值順序的代碼;