万字长文带你一文读完Effective C++
Effective C++
視C++為一個語言聯邦
STL Template C++ C Object-oriented C++
一開始C++只是C加上一些面向對象特性,但是隨著這個語言的成熟他變得更加無拘無束,接受不同于C with classes的各種觀念、特性和編程戰略。異常對函數的結構化帶來了不同的做法,templates將我們帶來到新的設計思考方式,STL則定義了一個前所未見的伸展性做法。
今天C++已經是個多重范型編程語言,一個同時支持過程形式、面向對象形式、函數形式、泛型形式、元編程形式的語言。這些能力和彈性使C++成為一個無可匹敵的工具,因此、將C++視為一個語言聯邦
盡量以cosnt、enum、inline替換#define
因為、宏定義會被預處理器處理,編譯器并未看到宏定義的信息,當出現一個編譯錯誤信息的時候,可能會帶來困惑。
解決之道就是使用一個常量替換宏定義(#define)
const double AspectRatio = 1.653; // 大寫名稱通常代表宏定義,因此這里可以使用首字母大寫的方法表示const全局變量作為一個語言常量,AspectRatio肯定會被編譯器看到,當然就會進入符號表內。另外、使用常量也可以有較小的碼、因為使用預處理會導致預處理器盲目的將宏名稱替換為對應的數值,可能會導致目標碼出現多份宏定義的數值。
基于數個理由enum hack值得我們認識。
- enum hack的行為某方面來說比較像#define而不像const,有的時候這正是你想要的,例如取一個const的地址是合法的,但是取一個enum的地址就是不合法的,而取一個#define的地址通常也不合法。如果你不想讓別人獲得一個pointer或者reference指向你的某個整數常量,enum可以幫助你實現這個約束。
- 雖然優秀的編譯器不會為const對象設置存儲空間,但是不夠優秀的編譯器可能會設置另外的儲存空間,enum和#define一樣絕對不會導致非必要的內存分配。
- 出于實用主義考慮,很多代碼特別是模板元編程中用到了它,因此、看到它你必須認識它。
對于單純的常量,最好以const對象或者enums替換#define
對于形似函數的宏(macros),最好改用inline函數替換#define
盡可能使用const
const允許你指定一個語義約束(也就是指定一個不該被改動的對象),編譯器會強制執行這一約束。
char greeting[] = "Hello"; char *p = greeting; // non-const pointer, non-const data const char* p = greeting; // non-const pointer, const data char* const p = greeting; // const pointer non-const data const char* const p = greeting; // const pointer, const dataconst語法雖然變化多端,但并不是莫測高深,如果關鍵字const出現在型號的左邊,表示被指物是常量,如果出現在星號的右邊,表示指針自身是常量,如果出現在星號兩邊,表示被指物和指針兩者都是常量。
如果被指物是常量,有些程序員會將關鍵字const寫在類型之前,有些人會把它寫在類型之后、星號之前,這兩種寫法的意義相同,所以下列兩個函數的參數類型是一樣的:
void f(const Widget* pw); // 一個指向常量的指針 void f2(Widget const* pw); // 一個指向常量的指針兩種形式都有人使用,是否是指向常量的指針,要看const相對于星號的位置,星號左邊為指向常量的指針,星號右邊為常量指針。
const修飾函數返回值,可以降低編碼出現的低級錯誤
class Rational {}; const Rational operator*(const Rational& lhs, const Rational& rhs); Rational a, b, c; if (a*b = c) // 其實是想做個比較,當operator*返回值聲明為const的時候將會返回錯誤,也就防止了編碼不小心帶來的異常const修飾成員函數
-
可以通過const得知哪些函數可以改動對象內容,哪些函數不可以
-
使得操作const對象成為可能
-
將某些東西聲明為const可以幫助編譯器偵測出錯誤的用法,const可以施加于任何作用于內的對象、函數參數、函數返回類型、成員函數本體
確定對象被使用前已先被初始化
關于將變量初始化這件事,C++似乎總是反復無常。但是有一點是可以確定的是,讀取沒有初始化的值會導致不確定行為。
- 對于內置型對象進行手工初始化,因為C++不保證初始化它們
- 構造函數最好使用成員初值列,而不要在構造函數中使用賦值操作,初始列次序應該和它們在class中的聲明次序相同
- 為了避免跨編譯單元初始化次序問題,請使用local static對象替換non-local static對象,
構造析構賦值
了解C++默默編寫并調用哪些函數
當你聲明一個空類的時候,C++處理互生成:
- copy構造函數
- Copy assignment操作符
- 析構函數
如果你寫:
class Empty {};經過處理:
class Empty { public:Empty() {} // default構造函數 Empty(const Empty& rhs) {} // copy 構造函數~ Empty() {} // 析構函數Empty& operator=(const Empty& rhs) {} // copy assignment };只有當這些函數被需要(被調用),他們才會被編譯器創建出來,
Empty e1; // default構造函數Empty e2(e1); // copy構造函數 e2 = e1; // copy assignment操作符若是不想使用編譯器自動生成的函數,就應該明確拒絕
為了駁回編譯器自動生成的代碼,可以使用將對應函數聲明為private,將對應函數后面加上delete,使用Uncopyable這樣的base class。
class Uncopyable { protected:Uncopyable() {}~ Uncopyable() {} private:Uncopyable(const Uncopyable&); // 阻止copingUncopyable& operator=(const Uncopyable&);};為多態聲明virtual析構函數
- 帶有多態性質的base class應該聲明一個virtual析構函數,如果沒有聲明,那么當父類指針指向子類對象,通過指針銷毀對象的時候不會調用子類的析構函數。
- 如果不具備多態性質,就不應該聲明虛析構函數,因為只要有虛函數就會存在虛函數列表,導致類的大小改變,每個函數的查找偏移變大,類中就算只有成員元素也不能按照結構體使用。
別讓異常逃離析構函數
有如下代碼:
class Widget { public:~Widget() {} // 假設可能吐出一個異常 };void doSomething() { std::vector<Widget> v; ...} // v在這里被銷毀當銷毀vector v的時候,有責任把所有的Widgets銷毀掉。假設析構第一個元素的時候拋出了異常,因為還有沒有析構的,所以取析構第二個,假設第二個也拋出了異常,這對C++來說異常跳多了,在兩個異常同時存在的情況下,程序不是結束執行就是導致不明確行為。
- 析構函數絕對不要吐出異常,如果一個被析構的函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然后吞下他們(不傳播)或結束程序
- 如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那么class應該提供一個普通函數執行該操作,而不是在析構函數中。
絕不在構造和析構函數過程中調用virtual函數
- 在base class構造期間,virtual函數不是virtual函數
- 當base cleass構造函數執行時,derived class的成員尚未初始化,如果此期間用的virtual函數下降至derived classes階層,要知道derived class函數幾乎必然取local成員變量,而那些成員變量尚未初始化,這將是一張通往不明確行為和徹夜調試大會的直達車票。
- Derived class對象的base class構造期間,對象的類型是base class而不是derived class,不只是virtual函數會被視為
- 在構造函數和析構函數期間不要調用virtual函數,因為這類調用從不下降至derived class
令operator= 返回一個reference to *this
關于賦值你可以寫成連鎖的形式:
int x, y, z; x = y = z = 9;如果想給一個類實現上述操作,類的復制操作符實現必須返回一個reference指向操作符的左側實參,這也是你為classes實現賦值操作符時應該遵循的協議:
注意這只是個協議,并沒有強制性,如果不遵循它代碼同樣是可以編譯通過的,然而這份協議被所有內置類型和標準程序庫提供的類型實現,如: string, vector, complex等,因此除非你有一個標新立異的好理由,否則就隨眾吧。
- 令賦值(assignment)操作符返回一個reference to *this
在operator=中處理"自我賦值"
自我賦值發生在對象被賦值給自己時:
class Widget { };Widget w; ... w = w;看起來很蠢,但是卻合法,并且賦值操作符并不是看起來那么明顯,如果在循環賦值中:
a[i] = a[j]; // 潛在的自我賦值,如果i == j那么這就是一個潛在的自我賦值自我賦值存在問題的operator=函數
Widget& Widget::operator=(const Widget& rhs) {delete pb; // 停止當前使用的bitmap,但是想象下,如果傳進來的是對象自身 ?pb = new Bitmap(*rhs.pb); // 使用復件,如果傳進來的是對象自身,那么上面已經刪除了pb指針了return *this;}傳統解決辦法,使用在函數前證同測試,達到自我賦值檢測目的
Widget& Widget::operator=(const Widget& rhs) {if (this == &rhs) return *this; // 如果是自我賦值,就直接返回自己的引用delete pb; pb = new Bitmap(*rhs.pb); return *this; }上述做法,當new Bitmap拋出異常時,還是會存在問題,我們可以按照下面的方式實現
Widget& Widget::operator=(const Widget& rhs) { // 即使new拋出異常也不影響正常使用Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb);delete pOrig; return *this; }使用交換的版本
Widget& Widget::operator=(const Widget rhs) { // 即使new拋出異常也不影響正常使用swap(rhs) return *this; }- 確保當對象進行自我賦值時operator=有良好的行為,其中技術包括比較來源對象和目標對象的地址、精心周到的語句順序、以及copy-and-swap
- 確定任何函數如果操作一個以上的對象,其中多個對象是同一個對象時,其行為仍然正確
復制對象時勿忘其每一個成分
設計良好的面向對象系統(OO-systems)會將對象內部分裝起來,只留兩個函數負責對象拷貝,那便是帶著名稱的copy構造函數和copy assignment操作符,我們稱之為copy函數。
如果使用編譯器生成的版本,那么編譯器將會對對象中所有的成員做一份拷貝,如果你自己實現了copy函數,那么編譯器就算你沒有復制所有對象的時候,還是能正常通過。
- copying函數應該確保復制對象所有的成員變量,以及所有的base class成分
- 不要嘗試以某個copying函數實現另一個copying函數,應該將共同機能放到第三個函數中,并由兩個copying函數共同調用
以對象管理資源
資源取得的時機便是初始化時機(Resource Acquisition Is Initialzation; RAIL)
- 獲得資源后立刻放進管理對象內
- 管理對象運用析構函數確保資源被釋放
- 為了防止資源泄漏,請使用RAIL對象,他們在構造函數中獲得資源,并在析構函數中釋放資源
在資源管理類中小心coping行為
- 禁止復制,許多時候允許RAIL對象被復制是不合理的
- 對底層資源使用引用計數法,有時候我們希望保有資源,直到它的最后一個使用者被銷毀
- 復制底部資源
- 轉移底部資源的所有權
- 復制RAIL對象必須一并復制它所管理的資源,所以資源的Copying行為決定RAIL對象的Copying行為
- 普遍而常見的RAIL class copying行為是:抑制copying、施行引用計數法
在資源管理類中提供對原始資源的訪問
資源放到資源管理類中是實現完美編程的必要手段,但是有些資源并非完美,需要將對應資源取出來直接使用
- APIs往往需要訪問原始資源,所以每一個RAIL class應該提供一個取得其所管理之資源的辦法
- 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但是隱式轉換對客戶比較方便
成對的使用new和delete時要采取相同形式
- 如果你在new表達式中使用了[],必須在delete表達式中也使用[]。如果你在new表達式中沒有使用[],一定不要在相應的delete表達式中使用[]。
以獨立語句將newed的對象置入智能指針
如下代碼:
int priprity(); void processWidget(std::share_ptr<Widget> pw, int priority);// 調用 processWidget(new Widget, priority());雖然上述資源采用了對象管理式資源,但是可能存在資源泄漏,執行的過程中,編譯器必須創建如下代碼:
C++以怎樣的次序生成上述代碼是不確認的,這也是C++與Java和C#的不同之處,C++經過編譯優化之后可能會生成如下代碼順序:
現在你想,如果priority函數拋出異常,將會導致資源泄露
- 以獨立語句將newed對象存儲只能指針內,如果不這樣做,一旦異常拋出,有可能有難以察覺的資源泄露
讓接口更容易被正確使用,不易被誤用
- 好的接口容易被正確使用不容易被誤用,你應該在你的所有接口中努力達到這樣的性質
- 促進正確使用的辦法包括接口的一致性以及內置類型的行為兼容
- 阻止誤用的辦法包括建立新類型、限制類型上的操作、束縛對象值,以及消除客戶的資源管理責任
設計class猶如設計type
C++就像其他OOP語言一樣,當你定義一個新的class也就定義了一個新的type。
- 新的type對象應該如何被創建和銷毀
- 對象初始化和對象復制應該有什么樣的區別
- 新的對象如果被pass by value意味著什么-copy 構造函數用來定義一個pass by value應該如何被實現
- 什么是新type的合法值
- 新type需要配合某個繼承圖系嗎
- 新type需要什么樣的轉換-隱式轉換還是顯示轉換
- 什么樣的操作符作用到新type上是合理的
- 什么樣標準函數應該駁回
- 誰該取用新type的成員-成員那個應該是public、protected、private。
- 什么是新type的未聲明接口
- 你的新type有多么一般化
- 你是否真的需要一個新type
- class的設計就是type的設計。在定義一個type之前請確定你已經認真考慮過
寧以pass-by-reference-to-const替換pass-by-value
- 盡量以pass-by-reference-to-const替換pass-by-value。前者比較高效,并且可以避免切割問題
- 以上規則并不適用于內置類型,以及STL的迭代器和函數對象,對他們而言pass-by-value往往比較適當
切割問題:當一個derived class對象被by value方式并且被視為base class對象,base class的copy構造函數將會被調用,而造成對象的行為像個derived對象的那些性質全被切割掉,僅留下一個base class對象。
C++編譯器的底層往往使用指針的形式實現引用,因此pass by reference通常意味著真正傳遞的是指針
必須返回對象時,別妄想返回其reference
- 絕對不要返回pointer或Reference指向一個local stack對象,或者返回引用指向一個heap-allocated對象,或返回pointer或reference指向一個local static對象而有可能同時需要多個這樣的對象
將成員變量聲明為private
- 切記將成員變量聲明為private,這樣可以賦予客戶訪問數據的一致性、可細微劃分訪問控制、允諾約束條件獲得保證,并提供class作者以充分實現彈性
- protected并不意味著比public更具有封裝性
寧以non-memeber、non-friend替換member函數
有時候使用一個non-memeber、non-friend函數,比使用Member函數根據有封裝性,比如一個清理瀏覽器資源歷史記錄和緩存的函數
// 因為clearBrowser 不能方位對象的內部成員變量,成員變量的操作只能經過傳進來的對象引用調用的函數操作,因此封住效果比成員函數更好 void clearBrowser(WebBrowser& wb) {wb.clearCache();wb.clearHistory();wb.removeCookies(); }- 寧以non-memeber、non-friend替換member函數,這樣可以增加封裝性、包裹彈性和技能擴充性
若所有參數皆需要類型轉換,請為此采用non-member函數
令classs支持隱式轉換通常是個糟糕的注意,當然這條規則也有例外的情況,假設你設計一個類用來表示有理數,允許整數的隱式轉換似乎頗為合理.
比如我們實現運算符*,要是將其實現為內置函數如下:
class Rational { public:const Rational operator* (const Rational& rhs) const; };以上實現方式,在使用能夠實現隱式轉換的函數時候,需要隱式轉換的參數只能放到后面,否則將會報錯
result = oneHalf * 2;// 正常 result = 2 * oneHalf; // 錯誤然給我們寫成函數調用的方式之后,問題就會一目了然了
result = oneHalf.operator*(2); // 很好 result = 2.operator*(oneHalf); // 錯誤,因為2沒有實現operator*,全局operator*并不能處理oneHalf的運算讓我們實現non-member函數
class Rational { public:}; const Rational operator* (const Rational& lhs,const Rational& rhs) {return Rational(lhs.numerator() * rhs.numerator(), lhs.dnominator() * rhs.denominator()) }寫成non-member函數之后
Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // 沒問題 result = 2 * oneFourth * 2; // 也沒問題- 如果你要為某個函數的所有參數(包括this指針所指的那個隱喻參數),進行類型轉換,那么這個函數必須是個non-member
考慮寫出一個不拋出異常的swap函數
- 當swap函數對你的類型效率不高時,提供一個swap成員函數,并確定這個函數不拋出異常
- 如果你提供一個member swap,也該提供一個Non-member swap用來調用前者,對于class(而非templates),也請特化std::swap
- 調用swap時應針對std::swap使用using聲明方式,然后調用swap并且不帶有任何空間資格修飾
盡可能延后變量定義式的出現時間
- 盡量延后變量定義式出現的時間,這樣可以增加程序的清晰度并改善程序效率
盡量少做轉型動作
- const_cast通常被用來將對象的常量性轉除(cast away the constness),它也是唯一一個具有此能力的C++Style轉型操作符
- dynamic_cast主要用來執行安全向下轉型(safe downcasting), 也就是用來決定某個對象是否歸屬繼承體系中的某個類型。他是唯一一個無法由舊式語法執行的動作,也是唯一一個可能耗費重大運行成本的轉型動作
- reinterpret_cast意圖執行低級轉型,實際動作可能取決于編譯器,這也就表示它不可移植。例如將一個pointer to int轉型為一個int。
- static_cast用來強迫隱式轉換,例如將Non-const對象轉換為const對象,或將int轉為double等。也可以執行上述多種轉換的反向轉換,唯一不能轉換的是將const轉換為非const
- 如果可以,盡量避免類型轉換,特別是在注重代碼效率的代碼中避免dynamic_casts,如果有個設計需要轉換類型動作,試著發展無需轉型的替代設計
- 如果轉型是有必要的,試著將其隱藏在某個函數背后,可以隨后可以調用該函數,而不需要將轉型放進他們自己的代碼內。
- 寧可使用C++style轉型,不要使用舊式轉型,前者很容易辨別出來
避免返回handles指向對象內部成分
- 避免返回handles(包括references、指針、迭代器)指向對象內部。遵守這個條款可增加封裝性,幫助const成員函數像個const。
為異常安全而努力是值得的
- 異常安全函數即使發生異常也不會泄漏資源或允許任何數據結構敗壞。這樣的函數區分為三種可能的保證:基本型、強烈型、不拋出異常型。
- 強烈保證往往能夠以copy-and-swap實現出來,但強烈保證并非對所有函數都可實現或具備現實意義
- 函數提供異常安全保證,通常最高只能等于其調用之各個函數的異常安全保證中的最弱者
透徹了解inlining的里里外外
- 將大多數inlining限制在小型、被頻繁調用的函數身上。這可使日后的調試過程和二進制升級更加容易,也可使潛在的代碼膨脹問題最小化,使得程序速度提升機會最大化
- 函數具體是否inlined還取決于調用方式,當使用指針調用的時候,即使你聲明了inline還是會按照非inline函數調用
- 不要只因為function templates出現在頭文件,就將其生命為inline
將文件間的編譯依存關系降至最低
- 支持依存性最小化的一般構想是:相依于聲明式,不要依于定義式,基于此構想的兩個手段是Handle和Interface classes
- 程序頭文件應該以完全且僅有聲明式的形式存在,這種做法不論是否涉及templates都適用
確定你的public繼承塑膜出is-a關系
- public繼承意味is-a。適用于base classes身上的每一件事情也都適用于derived classes身上,因為每一個derived class對象都是一個base class對象
避免遮掩繼承而來的名稱
class Derived : public Base { public:using Base::mf1; // 讓Base class內名為mf1的東西在子類中可見};- Derived classes內的名稱會遮掩base classes內的名稱。在public繼承下來沒有人希望如此
- 為了讓被遮掩的名稱再見天日。可使用using聲明式或轉交函數(forwarding functions)
區分接口繼承和實現繼承
- 接口繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的接口
- Pure virtual函數只具體指定接口繼承
- 簡樸的impure virtual函數具體指定接口繼承及缺省實現繼承
- non-virrtual函數具體指定接口繼承以及強制性實現繼承
考慮virtual函數以外的其他選擇
- virtual函數的替代放哪包括NVI(non-virtual interface)手法,以及strategy設計模式的多種形式。NVI手法自身是一個特殊形式的template method設計模式
- 將機能從成員函數移到class外部函數,帶來的一個缺點就是,非成員函數無法訪問class內部的non-static成員
絕不重新定義繼承而來的non-virtual函數
適用于子類對象的每一件事,也適用于D對象,因為每一個D對象都是一個B對象
子類一定會繼承父類的非虛函數
絕對不重新定義繼承而來的缺省參數值
virtual函數是動態綁定的,而缺省參數是靜態綁定的
- 絕對不要重新定義一個繼承而來的缺省參數值,因為缺省參數值都是靜態綁定的,而虛函數是動態綁定的
通過復合塑膜出has-a或根據某物實現出
- 復合的意義和public繼承完全不同
明智而審慎地使用private繼承
獨立非附屬的對象大小一定不為零。但是這個約束不適合derived class對象內的base class成分,因為他們非獨立,如果你繼承了Empty
class HoldsAnInt : private Empty { private:int x; }幾乎可以肯定的是sizeof(HoldsAnInt) == sizeof(int)。這里的EBO(empty base optimization, 空白基類最優化),該技術在STL中進行了大量的實踐,比如很多類進程的基礎類中沒有任何數據,但是有很多enum typedef的類型
- Private繼承意味著is-implemented-in-terms of根據某物實現出。它通常比復合的級別低。但是derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函數時,這么設計是合理的
- 和復合不同的是,private繼承可以造成empty base最優化。這對致力于對象尺寸最小化的程序開發者很重要
明智而審慎地使用多重繼承
- 多重繼承比單一繼承復雜。他可能導致新的歧義性,以及對virtual繼承的需要
- virtual繼承會增加大小、速度、初始化復雜度等等成本,如果virtual base calsses不帶任何數據,將是最具有價值的情況
- 多重繼承的確有正當用途。其中一個情節設計public 繼承某個Interface class和private繼承某個協助實現的class的兩相組合
了解隱式接口和編譯器多態
- Classes和templates都支持接口interfaces和多態
- 對classes而言接口是顯示的explicit,以函數簽名為中心。多態則是通過virtual函數發生于運行期
- 對template參數而言,接口是隱式(implicit)的,奠基于有效表達式,多態則是通過template具體化和函數重載解析(function overloading resolution)發生于編譯器
了解typename的雙重意義
雖然很多時候class可以和typename混用,但是有時候你一定得使用typename。
template<typename C> void print2nd(const C& container) {if (container.size() >= 2) {typename C::const_iterator iter(container.begin());...} }因為std::interator_traits::value_type是嵌套從屬名詞,因此必須在類型名前放置typename,不然編譯器不認得
但是每次都寫那么長一串,誰都受不了,因此我們需要在其前方加上typedef從新定義一個類型,因此你可以看到STL中存在大量的如下定義
teplate<typename iTerT> void wordWithIterator(IterT iter) {typedef typename std::interator_traits<IterT>::value_type temp(*iter);... }typename是為了編譯器能夠識別
typedef是為了程序員能夠減輕定義變量帶來的負擔
- 聲明template參數時,前綴關鍵字class和typename可互換
- 請使用關鍵字typename標識嵌套從屬類型名稱;但不得在base class lists基類列或member initialization list成員初始列內以它作為base class修飾符
學習處理模板化基類內名稱
- 可在derived class templates內通過this->指涉base class templates內的成員名稱,或藉由一個明白寫出的base class資格修飾符完成
將參數無關的代碼抽離templates
- templates生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數產生相依關系
- 因非類型模板參數(non-type template parameters)而造成的代碼膨脹,往往可消除,做法是以函數參數或class成員變量替換template參數
應用成員函數模板接受所有兼容類型
同一個template的不同具體實現之間并不存在什么與生俱來的固有關系,即使模板是使用具有繼承關系的兩個類進行實例化的。
模板與泛化編程(Generic Programming)
template<typename T> class SmartPtr { public:template<typename U> // 成員member templateSmartPtr(const SmartPtr<U>& other); // 為了生成copy構造函數 };以上代碼的意思是,對于任何類型T和任何類型U,這里可以根據SmartPtr生成一個SmartPtr,因為SmartPtr有個構造函數接受一個SmartPtr參數。這一類構造函數根據對象u創建對象t,而u和v的類型是同一個template的不同具體體現,有時候我們稱之為泛化(generalized)copy構造函數。
- 請使用member function templates成員函數模板生成可接受所有兼容類型的函數
- 如果你聲明member templates用于泛化copy 構造或泛化assigment操作,你還是 需要證明正常的copy構造函數和copy assigment操作符,因為如果你不聲明,當編譯器根據模板并不能實現對應的構造函數的時候,將會按照默認規則生成對應的默認copy函數和assigment操作符
需要類型轉換時請為模板定義非成員函數
template實參推導過程不將隱式轉換考慮在內,絕不!
當我們編寫一個class template,而它所提供之"與template相關的"函數支持所有參數之隱式類型轉換時,請將那函數定義為class template內部的friend函數。
請使用traits calsses表現類型信息
結合該場景只要類型足夠多可以在編譯期間實現if…else…
iterator_traits的運作方式是,針對每一個類型,在struct iterator_traits內聲明某個typedef名為iterator_catory,這個typedef用來確認IterT的迭代器分類
如deque的迭代器,定義如下:
struct _Deque_iterator {...typedef std::random_access_iterator_tag iterator_category;... }通過iterator_catory我們可以獲得數據的類型,我們通過獲取的類型創建對象,然后在根據重載實現在不同類型調用不同的函數。
1. advance函數內部調用函數 std::__advance(__i, __d, std::__iterator_category(__i));2. iterator_category根據iterator_catory返回對應類型的一個對象 template<typename _Iter>inline _GLIBCXX_CONSTEXPRtypename iterator_traits<_Iter>::iterator_category__iterator_category(const _Iter&){ return typename iterator_traits<_Iter>::iterator_category(); }3. 然后根據重載,實現各個類型所需調用的函數,到時候會根據具體的類型調用對應的函數 template<typename IterT, typename DistT> // 這份實現用于random access void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {iter += d; }template<typename IterT, typename DistT> // 這份實現用于didirectional void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {if (d >= 0) {while(d --) ++iter;} }template<typename IterT, typename DistT> // 這份實現用于input迭代器 void doAdvance(IterT& iter, DistT d, std::iput_iterator_tag) {iter += d; }template<typename IterT, typename DistT> // 這份實現用于random access void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {iter += d; }- Traits classes使得類型相關信息在編譯期間可用,它們以templates和templates特化實現
- 結合重載技術,traits calsses有可能在編譯期間對類型執行if …else…
認識template元編程
Template metaprogramming(TMP, 模板元編程)是編寫template-based C++程序并執行于編譯期的過程。所謂的模板元編程是以C++編寫成、執行于C++編譯器內的程序。一旦TMP程序執行結束,其輸出,也就是從templates具體出來的若干C++源碼,便會一如往常的被編譯。
template編程,簡稱函數式語言,主要依賴于遞歸,但是這個遞歸不是真正的函數遞歸調用,而是遞歸模板化具體化。
#include <iostream>using namespace std;template<unsigned n> struct Factorial {enum {value = n * Factorial<n - 1>::value}; };// 特化 template<> struct Factorial<0> {enum {value = 1}; };int main(int argc, char *argv[]) {cout << Factorial<6>::value << endl;cout << Factorial<3>::value << endl;return 0; }- Template metaprogramming(TMP, 模板元編程)可將工作由運行期移往編譯期,因而得以實現早期錯誤偵測和更高效的執行效率。
- TMP可被用來生成基于政策選擇組合的客戶定制代碼,可用來避免生成對某些類型并不適合的代碼。
了解new-handler的行為
set_new_handler的參數是個指針,指向operator new無法分配足夠內存時該被調用的函數,其返回值也是個指針,指向set_new_handler被調用前正執行的那個new-handler函數
- set_new_handler允許客戶指定一個函數,在內存分配無法獲得滿足時被調用
- Nothrow new是一個頗為局限的工具,因為它只適用于內存分配,后續的構造函數還是可能拋出異常
了解new和delete的合理替換時機
有以下情況時可能需要替換編譯器提供的operator new和operator delete
- 有許多理由需要寫個自己定制的new和delete,包括改善效能、對heap運用錯誤進行調試、手機heap使用信息。
編寫new和delete時需固守常規
Operator new的必須返回正確的值,內存不足時必須得調用new-handling函數,必須應對零內存修的準備,還需要避免掩蓋正常形式的new。operator new的返回值要十分單純如果有能力應答客戶申請的內存,就返回一個指針指向那塊內存,如果沒有那個能力就拋出一個bad_alloc的異常。
當然operator new也不是十分的單純,在實際分配內存時,會不止一次的嘗試分配內存,并且在每次失敗后調用new-handling函數,只有當指向new-handling函數的指針是null時,operator new才會拋出異常
- Operator new應該內含一個無窮循環,并在其中嘗試分配內存,如果它無法滿足內存需求,就該調用new-handler,它也應該有能力處理0 bytes的申請,class專屬
- Operator delete應該收到null指針時不做任何事情,class專屬版本還應該處理比正確大小更大的處理
寫了palcement new也要寫placement delete
Widget *pw = new Widget;s上述代碼一共會有兩個函數被調用,一個是用于分配內存operator new,一個是Widget的default構造函數
假設第一個函數調用成功,第二個函數調用的時候出現了異常,這時需要將步驟一申請的內存回復到原觀,否則就會造成內存泄露,但是在這時候因為客戶沒有拿到pw所以取消步驟一并回復舊觀的責任就落到了C++運行期系統上。
Placement是帶有額外參數的意思
- 當你寫一個placement operator new,請確定也寫出了對應的placement operator delete。如果沒有這樣做,你的程序可能發生隱微而時斷時續的內存泄露
- 當你聲明palcement new和placement delete請確定不要無意識地遮掩他們的正常的版本
不要忽略編譯器的警告
- 嚴肅對待編譯器發出的警告信息,努力在你的編譯器的最高警告登記下爭取無任何警告的榮譽
- 不要過度依賴編譯器報警能力,因為不同編譯器對待事情的態度并不相同。一旦移植到另外一個編譯器上,你原有依賴的編譯警告信息就有可能消失
讓自己熟悉包括TR1在內的標準程序庫
讓自己熟悉Boost
提供PDF下載地址:
PDF版本下載地址
總結
以上是生活随笔為你收集整理的万字长文带你一文读完Effective C++的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 移动语义-右值引用-完美转发-万字长文让
- 下一篇: s3c2440移植MQTT