Effective C++ 55個條款筆記
一、讓自己習慣C++
1.視C++為一個語言聯邦
請記住:
C++高效程式設計守則視狀況而變化,取決於你使用C++的哪一部分。
2.儘量以const,enum,inline替換#define
寧可以編譯器替換前處理器
string通常比 char * 更好
類的靜態常量(static const),在類中宣告(標頭檔案),類外定義(實現檔案)
“enum hack“:一個屬於列舉型別的數值可權充ints使用
例:
class GamePlayer {
private:
enum {NumTurns = 5}; // "the enum hack"——令NumTurns成為5的一個記號名稱
int scores[NumTurns];
};
enum hack的行為某方面比較像#define而不像const,enum hack是模版超程式設計的基礎技術
模版行內函數代替巨集
請記住:
對於單純常量,最好以const物件或enums替換#defines。
對於形似函式的巨集,最好改用inline函式替換#defines。
3.儘可能使用const
如果const出現在*左邊,表示被指物是常量;如果出現在*右邊,表示指標自身是常量。
STL迭代器的作用類似於 T* 指標
const vector<int>::iterator相當於 T* const,迭代器本身是const
vector<int>::const_iterator相當於const T*,迭代器所指向的東西不可改動
令函式返回一個常量值
除非有需要改動引數或區域性物件,否則將引數宣告為const
const成員函式
兩個成員函式如果指示常量性不同,可以被過載
mutable可變的
const_cast:轉型,消除物件的const屬性
請記住:
將某些東西宣告為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的物件、函式引數、函式返回型別、成員函式本體。
編譯器強制實施bitwise constness,但你編寫程式時應該使用“概念上的常量性”(conceptual constness)。
當cosnt和non-const成員函式有著實質等價的實現時,令non-const版本呼叫const版本可避免程式碼重複。
4.確定物件被使用前已先被初始化
永遠在使用物件之前先將它初始化
確保每一個建構函式都將物件的每一個成員初始化
別混淆了賦值和初始化
c++規定,物件的成員變數的初始化動作發生在進入建構函式本體之前
預設構造一個成員變數時,也可以用成員初始化列表,只要指定()作為初始化實參即可
父類比子類先初始化
類的成員變數總是以宣告次序被初始化
c++對“定義於不同的編譯單元內的non-local static物件”的初始化相對次序無明確定義
c++保證,函式內的local static物件會在“該函式被呼叫期間”“首次遇上該物件之定義式”時被初始化
單例模式:將每個non-local static物件搬到自己的專屬函式內(該物件在此函式內被宣告為static),這些函式返回一個引用指向它所含的物件。換句話說,用local static替換non-local static
請記住:
為內建物件進行手工初始化,因為C++不保證初始化它們。
建構函式最好使用成員初始化列表,而不要在建構函式本體內使用賦值操作。初始化列表列出的成員變數,其排列次序應該和它們在類中的宣告次序相同。
為免除“跨編譯單元之初始化次序”問題,請以local static物件替換non-local static物件。
二、構造/析構/賦值運算
5.瞭解C++默默編寫並呼叫哪些函式
預設建構函式、拷貝建構函式、拷貝賦值操作符,以及解構函式
所有這些函式都是public且inline的
唯有當這些函式被呼叫時,才會被編譯器創建出來
編譯器產出的解構函式是個non-virtual
手動聲明瞭這些函式,編譯器就不會建立
預設拷貝建構函式執行的是淺拷貝
如果類中含有引用成員或const成員,編譯器拒絕生成預設的拷貝賦值操作符
如果基類的拷貝賦值運算子是私有的,派生類就不會生成預設的拷貝賦值運算子
請記住:
編譯器可以暗自為類建立預設建構函式、拷貝建構函式、拷貝賦值操作符,以及解構函式。
6.若不想使用編譯器自動生成的函式,就該明確拒絕
c++11中可以宣告為=delete來阻止拷貝
請記住:
為駁回編譯器自動(暗自)提供的機能,可將相應的成員函式宣告為private並且不予實現。使用像Uncopyable這樣的基類也是一種做法。
7.為多型基類宣告virtual解構函式
任何類只要帶有虛擬函式都幾乎確定應該也有一個虛解構函式
vptr(virtual table pointer)指標指向一個有函式指標構成的陣列,稱為vtbl(virtual table),每一個帶有虛擬函式的類都有一個相應的vtbl。當物件呼叫某一個虛擬函式,實際被呼叫的函式取決於該物件的vptr所指的那個vtbl——編譯器在其中尋找適當的函式指標。
為抽象類宣告一個純虛解構函式,並在類外提供一份定義,這個純虛擬函式的定義不會實現
請記住:
帶有多型性質的基類應該宣告一個virtual解構函式。如果一個類帶有任何virtual函式,它就應該擁有一個virtual解構函式。
一個類的設計目的不是作為基類使用,或不是為了具備多型性,就不該宣告virtual解構函式。
8.別讓異常逃離解構函式
並不禁止解構函式吐出異常
請記住:
解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕捉任何異常,然後吞下它們(不傳播)或結束程式。
如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼類應該提供一個普通函式(而非在解構函式中)執行該操作。
9.絕不在構造和析構過程中呼叫virtual函式
建構函式和解構函式中不要呼叫虛擬函式,它們所呼叫的所有函式中也不要呼叫虛擬函式
基類的構造先於派生類的構造,在派生類的物件的基類構造期間,物件的型別是基類而不是派生類
基類的析構後於派生類的析構,派生類先析構了,基類析構的時候可能再次析構派
請記住:
在構造和解構函式期間不要呼叫虛擬函式,因為這類呼叫從不下降至派生類。
10.令operator= 返回一個reference to *this
返回一個引用指向左側實參
所有賦值相關的運算:+=,-=,*=等等
這只是一個協議,並無強制性
請記住: 令賦值操作符返回一個reference to *this。
11.在operator=中處理“自我賦值”
“別名”就是有一個以上的方法指稱某物件
如果某段程式碼操作指標或引用而它們被用來指向多個相同型別的物件,就需考慮這些物件是否是同一個
copy and swap
請記住:
確保當物件自我賦值時operator =有良好行為。其中技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、以及copy-and-swap。
確定任何函式如果操作一個以上的物件,而其中多個物件是同一個物件時,其行為仍然正確。
12.複製物件時勿忘其每一個成員
當你編寫一個拷貝建構函式或拷貝賦值運算子時,請確保:複製所有區域性成員變數,呼叫所有基類的適當的拷貝函式(在初始化列表中呼叫)
不要用拷貝建構函式呼叫拷貝賦值運算子
不要用拷貝賦值運算子呼叫拷貝建構函式
請記住:
Copying函式應該確保複製“物件內的所有成員變數”及“所有基類成員”。
不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。
三、資源管理
所謂資源就是,一旦用了它,將來必須還給系統。
記憶體只是必須管理的眾多資源之一,其他常見的資源包括:互斥鎖,資料庫連線,socket連線。
13.以物件管理資源
RAII(Resource Acquisition Is Initialization),也稱為“資源獲取就是初始化”。
把資源放進物件內,便可以依賴c++的“解構函式自動呼叫機制”確保資源被釋放。
獲取資源後立刻放進管理物件內,管理物件運用解構函式確保資源被釋放。
請記住:
為防止資源洩漏,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源。
兩個常被使用的RAII類分別是auto_ptr和tr1::shared_ptr。後者通常是較佳選擇,因為其拷貝行為比較直觀。若選擇auto_ptr,複製動作會使他(被複制物)指向NULL。
14.在資源管理類中小心拷貝行為
複製資源管理物件時,進行的是“深度拷貝”。
請記住:
複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為。
普遍而常見的RAII類拷貝行為是:抑制拷貝,施行引用計數法。不過其它行為也可能被實現。
15.在資源管理類中提供對原始資源的訪問
請記住:
APIs往往要求訪問原始資源,所以每一個RAII類應該提供一個“取得其所管理之資源”的方法。
對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。
16.成對使用new和delete時要採取相同形式
陣列所用的記憶體通常還包含著“陣列大小“的記錄
如果對一個物件呼叫delete[],結果是未定義的;
如果對一個物件陣列呼叫delete,結果是未定義的,可能是太少的解構函式被呼叫。
最好不要對陣列形式做typedef動作
請記住:
如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果你在new表示式中不使用[],一定不要在相應的delete表示式中使用[]。
17.以獨立語句將newed物件置入智慧指標
請記住:
以獨立語句將newed物件儲存於(置入)智慧指標內。如果不這樣做,一旦異常被丟擲,有可能導致難以察覺的資源洩漏。
四、設計與宣告
18.讓介面容易被正確使用,不易被誤用
請記住:
好的介面很容易被正確使用,不容易被誤用。你應該在你的所有介面中努力達成這些性質。
“促進正確使用”的辦法包括介面的一致性,以及與內建型別的行為相容。
“阻止誤用”的辦法包括建立新型別、限制類型上的操作,束縛物件值,以及消除客戶的資源管理責任。
tr1::shared_ptr支援定製刪除器。這可防範DLL問題,可被用來自動解除互斥鎖等等。
19.設計class猶如設計type
請記住:
Class的設計就是type的設計。在定義一個新的type之前,請確定你已經考慮過本條款覆蓋的所有討論主題。
20.寧以pass-by-reference-to-const替代psss-by-value
c++中的引用往往以指標實現,內建型別可能比指標小,這時傳值就比傳引用好
請記住:
儘量以pass-by-reference-to-const替代pass-by-value。前者通常比較高效,並可避免切割問題。
以上規則並不使用於內建型別,以及STL的迭代器和函式物件。對它們而言,pass-by-value往往比較適當。
21.必須返回物件時,別妄想返回其reference
請記住:
絕不要返回pointer或reference指向一個local stack物件,或返回reference指向一個heap-allocated物件,或返回pointer或reference指向一個local static物件而有可能同時需要多個這樣的物件。
22.將成員變數宣告為private
一致性,如果成員變數不是public,客戶唯一能夠訪問物件的辦法是通過成員函式;
使用函式可以讓你對成員變數的處理有更精確的控制;
不封裝意味著不可改變;
成員變數的封裝性與“成員變數的內容改變時破壞的程式碼量”成反比;
只有兩種訪問許可權:private(封裝)和不封裝
請記住:
切記將成員變數宣告為private。這可賦予客戶訪問資料的一致性、可細微劃分訪問控制、允許約束條件獲得保證,並提供class作者以充分的實現彈性。
protected並不比public更具封裝性。
23.寧以non-member、non-friend替換member函式
將所有便利函式放在多個頭檔案內但隸屬於同一個名稱空間,意味著客戶可以輕鬆擴充套件這一組便利函式
請記住:
寧可拿non-member non-friend函式替代member函式。這樣做可以增加封裝性、包裹彈性和機能擴充性。
24.若所有引數皆需型別轉換,請為此採用non-member函式
成員函式的反面是非成員函式,不是友元函式。
請記住:
如果你需要為某個函式的所有引數(包括被this指標所指的那個隱喻引數)進行型別轉換,那麼這個函式必須是個non-member。
25.考慮寫出一個不拋異常的swap函式
請記住:
當std::swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常。
如果你提供一個member swap,也該提供一個non-member swap用來呼叫前者。對於class(而非templates),也請特化std::swap。
呼叫swap時應針對std::swap使用using宣告式,然後呼叫swap並且不帶任何“名稱空間資格修飾”。
為“使用者定義型別”進行std templates全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西。
五、實現
26.儘可能延後變數定義式的出現時間
延後變數定義直到能夠給它初值實參
變數定義在迴圈內還是在迴圈外?如果變數的賦值成本比“構造+析構”的低,那麼把變數定義在迴圈內;把變數定義在迴圈被,變數的範圍限制在迴圈內,作用域小。
請記住:
儘可能延後變數定義式的出現。這樣做可增加程式的清晰度並改善程式效率。
27.儘量少做轉型動作
請記住:
如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計。
如果轉型是必要的,試著將它隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需將轉型放進他們自己的程式碼內。
寧可使用C++-style(新式)轉型,不要使用舊式轉型。前者很容易辨識出來,而且也比較有著分門別類的執掌。
28.避免返回handls指向物件內部成分
dangling handles:這種handles所指的東西不復存在
請記住:
避免返回handles(包括references、指標、迭代器)指向物件內部。遵守這個條款可增加封裝性,幫助const成員函式的行為像個const,並將發生“虛吊號碼牌”(dangling handles)的可能性降至最低。
29.為“異常安全”而努力是值得的
請記住:
異常安全函式(Exception-safe functions)即使發生異常也不會洩漏資源或允許任何資料結構敗壞。這樣的函式區分為三種可能的保證:基本型、強烈型、不拋異常型。
“強烈保證”往往能夠以copy-and-swap實現出來,但“強烈保證”並非對所有函式都可實現或具備現實意義。
函式提供的“異常安全保證”通常最高只等於其所呼叫之各個函式的“異常安全保證”中的最弱者。
30.透徹瞭解inlining的裡裡外外
inline只是對編譯器的申請,不是強制命令
在類中定義的函式是隱式內聯的
inline函式通常一定置於標頭檔案內
inline是編譯期行為
一個表面看上去inline的函式是否inline取決於編譯器
大部分偵錯程式對inline函式都束手無策
不希望inline的函式定義在類外部,比如建構函式和解構函式,即使這兩者為空,也將空定義放在類的外部
請記住:
將大多數inlining限制在小型、被頻繁呼叫的函式身上。這可使日後的除錯過程和二進位制升級更容易,也可使潛在的程式碼膨脹問題最小化,是程式的速度提升機會最大化。
不要只因為function templates出現在標頭檔案,就將它們宣告為inline。
31.將檔案間的編譯依存關係降至最低
如果使用物件的引用或指標可以完成任務,就不要使用物件
如果能夠,儘量以類的宣告式替換類的定義式
為宣告式和定義式提供不同的標頭檔案
Handles class 和 Interface class解除了介面和實現之間的耦合關係,從而降低了檔案間的編譯依存性。代價是:在執行期喪失若干速度,為每個物件超額付出若干記憶體。
請記住:
支援“編譯依存性最小化”的一般構想是:相依於宣告式,不要相依於定義式。基於此構想的兩個手段是Handle classed和Interface classes。
程式庫標頭檔案應該以“完全且僅有宣告式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及templates都適用。
六、繼承與面向物件設計(重要)
32.確定你的public繼承塑模出is-a關係
請記住:
“public繼承”意味is-a。適用於base classes身上的每一件事情一定也使用於derived classes身上,因為每一個derived classes物件也都是一個base classes物件。
33.避免遮掩繼承而來的名稱
請記住:
derived calsses內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如此。
為了讓被遮掩的名稱再見天日,可使用using宣告式或轉交函式(forwarding function)。
34.區分介面繼承和實現繼承
純虛擬函式必須在派生類中重新定義
純虛擬函式在基類中也可以有定義,這是一種預設實現方式,但是在派生類中還是需要重新定義,派生類想要繼承預設實現就在重新定義的函式中呼叫基類的該函式
請記住:
介面繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的介面。
pure virtual函式只具體制定介面繼承。
簡樸的(非純)impure virtual函式具體制定介面繼承及預設實現繼承。
non-virtual函式具體制定介面繼承以及強制性實現繼承。
35.考慮virtual函式以外的其它選擇
當你為解決問題而尋找某個設計方法時,不妨考慮虛擬函式的替代方案
請記住:
virtual函式的替代方案包括NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式。
將機能從成員函式移到class外部函式,帶來的一個缺點是,非成員函式無法訪問class的non-public成員。
tr1::function物件的行為就像一般函式指標。這樣的物件可接納“與給定之目標籤名式(target signature)相容”的所有可呼叫物(callable entities)。
36.絕不重新定義繼承而來的non-virtual函式
請記住:
絕對不要重新定義繼承而來的non-virtual函式。
37.絕不重新定義繼承而來的預設引數值
請記住:
絕對不要重新定義一個繼承而來的預設引數值,因為預設引數值都是靜態繫結,而virtual函式——你唯一應該覆寫的東西——卻是動態繫結。
38.通過複合塑模出has-a或“根據某物實現出”
複合就是組合
請記住:
複合(composition)的意義和public繼承完全不同。
在應用域(application domain),複合意味has-a(有一個)。在實現域(implementation domain),複合意味is-implemented-in-terms-of(根據某物實現出)。
39.明智而審慎地使用private繼承
private繼承的每樣東西在派生類中都是private的
private繼承意味著只有實現部分被繼承,介面部分應略去
儘可能使用組合,必要時才使用private繼承
請記住:
Private繼承意味著is-implemented-in-terms of(根據某物實現出)。它通常比複合的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函式時,這麼設計是合理的。
和複合(composition)不同,private繼承可以造成empty base最優化。這對致力於“物件尺寸最小化”的程式庫開發者而言,可能很重要。
40.明智而審慎地使用多重繼承
第一,非必要不使用虛基類;第二,如果必須使用虛基類,避免在其中放資料。
請記住:
多重繼承比單一繼承複雜。它可能導致新的歧異性,以及對virtual繼承的需要。
virtual繼承會增加大小、速度、初始化(及賦值)複雜度等等成本。如果virtual base classes不帶任何資料,將是最具實用價值的情況。
多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相組合。
七、模版與泛型程式設計
41.瞭解隱式介面和編譯期多型
請記住:
classes和templates都支援介面(interfaces)和多型(polymorphism)。
對classes而言介面是顯式的(explicit),以函式簽名為中心。多型則是通過virtual函式發生於執行期。
對template引數而言,介面是隱式的(implicit),奠基於有效表示式。多型則是通過template具現化和函式過載解析(function overloading resolution)發生於編譯期。
42.瞭解typename的雙重意義
請記住:
宣告template引數時,字首關鍵字class和typename可互換。
請使用關鍵字typename標識巢狀從屬型別名稱;但不得在base class lists(基類列)或member initialization list(成員初值列)內以它作為base class修飾符。
43.學習處理模板化基類內的名稱
請記住:
可在derived class templates內通過“this->”指涉base class templates內的成員名稱,或藉由一個明白寫出的“base class資格修飾符”完成。
44.將與引數無關的程式碼抽離templates
請記住:
Templates生成多個classes和多個函式,所以任何template程式碼都不該與某個造成膨脹的template引數產生相依關係。
因非型別模板引數(non-type template parameters)而造成的程式碼膨脹,往往可消除,做法是以函式引數或class成員變數替換template引數。
因型別引數(type parameter)而造成的程式碼膨脹,往往可降低,做法是讓帶有完全相同二進位制表述(binary representations)的具體型別(instantiation types)共享實現碼。
45.運用成員函式模板接受所有相容型別
請記住:
請使用member function templates(成員函式模板)生成“可接受所有相容型別”的函式。
如果你宣告member templates用於“泛化copy構造”或“泛化assignment操作”,你還是需要宣告正常的copy建構函式和copy assignment操作符。
46.需要型別轉換時請為模板定義非成員函式
請記住:
當我們編寫一個calss template,而它所提供之“與此template相關的”函式支援“所有引數之隱式型別轉換”時,請將那些函式定義為“class template內部的friend函式”。
47.請使用traits classes變現型別資訊
請記住:
Traits classes使得“型別相關資訊”在編譯期可用。它們以templates和“templates特化”完成實現。
整合過載技術(overloading)後,traits classes有可能在編譯期對型別執行if…else測試。
48.認識template超程式設計
請記住:
Template metaprogramming(TMP,模板超程式設計)可將工作由執行期移往編譯期,因為得以實現早起錯誤偵測和更高的執行效率。
TMP可被用來生成“基於政策選擇組合”(based on combinations of policy choices)的客戶定製程式碼,也可用來避免生成對某些特殊型別並不合適的程式碼。
八、定製new和delete
49.瞭解new-handler的行為
請記住:
set_new_handler允許客戶指定一個函式,在記憶體分配無法滿足時被呼叫。
Nothrow new是一個頗為侷限的工具,因為它只適用於記憶體分配;後繼的建構函式呼叫還是可能丟擲異常。
50.瞭解new和delete的合理替換實際
請記住:
有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行除錯、收集heap使用資訊。
51.編寫new和delete時需固守常規
請記住:
operation new應該內含一個無窮迴圈,並在其中嘗試分配記憶體,如果它無法滿足記憶體需求,就該呼叫new-handler。它也應該有能力處理0 bytes申請。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。
operation delete應該在收到null指標時不做任何事。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。
52.寫了placement new也要寫placement delete
請記住:
當你寫一個placement operator new,請確定也寫出了對應的placement operator delete。如果沒有這樣做,你的程式可能會發生隱微而時斷時續的記憶體洩漏。
當你宣告placement new和placement delete,請確定不要無意識(非故意)地遮掩了它們的正常版本。
九、雜項討論
53.不要輕忽編譯器的警告
請記住:
嚴肅對待編譯器發出的警告資訊。努力在你的編譯器的最高(最嚴苛)警告級別下爭取“無任何警告”的榮譽。
不要多度依賴編譯器的報警能力,因為不同的編譯器對待事情的態度並不相同。一旦移植到另一編譯器上,你原來依賴的警告資訊有可能消失。
54.讓自己熟悉包括TR1在內的標準程式庫
請記住:
C++標準程式庫的主要機能由STL、iostreams、locales組成。幷包含C99標準程式庫。
TR1添加了智慧指標(例如tr1::shared_ptr)、一般化函式指標(tr1::function)、hash-based容器、正則表示式(regular expression)以及另外10個元件的支援。
TR1自身只是一份規範。為獲得TR1提供的好處,你需要一份實物。一個好的實物來源是Boost。
55.讓自己熟悉Boost
請記住:
Boost是一個社群,也是一個網站。致力於免費、原始碼開放、同僚複審的C++程式庫開發。Boost在C++標準化過程中扮演深具影響力的角色。
Boost提供許多TR1元件實現品,以及其他許多程式庫。