《Effective C++》读书笔记(第一部分)
本書并沒有你告訴什么是C++語言,怎樣使用C++語言,而是從一個經驗豐富的C++大師的角度告訴程序員:怎么樣快速編寫健壯的,高效的,穩定的,易于移植和易于重用的C++程序。
本書共分為9節55個條款,從多個角度介紹了C++的使用經驗和應遵循的編程原則。
本系列文章分兩部分概括介紹了《Effective C++》每個條款的核心內容,本文是第一部分,第二部分為:《Effective C++》讀書筆記(第二部分)。
1. 讓自己習慣C++(Accustoming your self to C++)
條款01: 視C++ 為一個語言聯邦
本條款提示讀者,C++已經不是一門很單一的語言,而應該將之視為一個由相關語言組成的聯邦。從語言形式上看,它是一個多重范型編程語言(multiparadigm programminglanguage) ,一個同時支持過程形式(procedural)、面向對象形式(object-oriented)、函數形式(functional) 、泛型形式(generic) 、元編程形式(metaprogramming )的語言,從語言種類上看,它由若干次語言組成,分別為:
(1) C。說到底C++ 仍是以C 為基礎。區塊(blocks) 、語句( statements) 、預處理器( preprocessor) 、內置數據類型(built-in data types) 、數組(arrays) 、指針(pointers) 等統統來自C。
(2) Object-Oriented C++。這部分也就是C with Classes 的: classes (包括構造函數和析構函數) ,封裝( encapsulation) 、繼承( inheritance) 、多態(polymorphism) 、virtual 函數(動態綁定) ……
(3) Template C++。這是C++ 的泛型編程(generic programming) 部分,也是大多數程序員經驗最少的部分。Template 相關考慮與設計己經彌漫整個C++,實際上由于templates 威力強大,它們帶來嶄新的編程范型(programming paradigm) ,也就是所謂的templatemetaprogramming (TMP,模板元編程)
(4) STL。 STL 是個template 程序庫,它是非常特殊的一個。它對容器(containers) 、迭代器(iterators) 、算法(algorithms) 以及函數對象(function objects) 的規約有極佳的緊密配合與協調。
條款02: 盡量以const, enum, inline替換#define
本條款討論了C語言中的#define在C++程序設計中的帶來的問題并給出了替代方案。
C語言中的宏定義#define只是進行簡單的替換,對于程序調試,效率來說,會帶來麻煩,在C++中,提倡使用const,enum和inline代替#define;然而,有了consts 、enums 和inlines,我們對預處理器(特別是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而#ifdef/#ifndef 也繼續扮演控制編譯的重要角色。目前還不到預處理器全面引迫的時候。
條款03: 盡可能使用const
本條款總結了Const的使用場景和使用它帶來的好處。
關鍵字canst 多才多藝。你可以用它在classes 外部修飾global 或namespace作用域中的常量,或修飾文件、函數、或區塊作用域(block scope) 中被聲明為static 的對象。你也可以用它修飾classes 內部的static 和non-static 成員變量。面對指針,你也可以指出指針自身、指針所指物,或兩者都(或都不〉是const。你應該盡可能地使用const,這樣降低程序錯誤,使程序易于理解。
此外,一個編程技巧是:當const 和non-const 成員函數有著實質等價的實現時,令non-const 版本調用const 版本可避免代碼重復:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class TextBlock { ?public: ??const char& operator[] (std::size_t position) const { ??…… ??return text[position]; ?} ?char& operator[] (std::size t position)? { ??return const_cast<char&>( static_cast<const TextBlock&>(*this) [position]); ??//將op[]返回值的const 轉除為*this 加上cons, 調用const op[] } |
條款04: 確定對象被使用前已先被初始化
本條款告誡程序員,在C++程序設計中,應該對所有對象初始化,以避免不必要的錯誤,同時,給出了高效初始化對象的方法和正確初始化對象的方法。
(1)初始化構造函數最好使用成員初值列(member initialization list) ,而不要在構造函數本體內使用賦值操作(assignment) 。初值列出的成員變量,其排列次序應該和它們在class 中的聲明次序相同。
考慮一個用來表現通訊簿的class ,其構造函數如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class PhoneNumber { ... }; class ABEntry { //ABEntry =“Address Book Entry" ?public: ??ABEntry(const std::string& name, const std::string& address , const std::list<PhoneNumber>& phones); ?private: ??std::string theName; ??std::string theAddress; ??std::list<PhoneNumber> thePhones; ??int numTimesConsulted; }; ABEntry: :ABEntry(const std: :string& nane , const std: : string& address, const std::list<PhoneNumber>& phones) ??theName = narne; //這些都是賦值(assignments) , ??theAddress = address; //不是始化(initializations)。 ??thePhones = phones; ??numTimesConsulted = 0; ??int num TimesConsulted; } |
正確而又高效的初始化對象的方法是:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | ABEntry: :ABEntry(const std: :string& nane , const std: : string& address, const std::list<PhoneNumber>& phones) : theName(name), theAddress(address), //這些都是初始化 thePhones(phones), numTimesConsulted(0) {} // 構造函數體是空的 |
C++ 有著十分固定的”成員初始化次序”。次序總是相同: base class早于其derived classes 被初始化,而class 的成員變量總是以其聲明次序被初始化?;仡^看看ABEntry. 其theName 成員永遠最先被初始化,然后是theAddress,再來是thePhones,最后是numTimesConsulted。即使它們在成員初值列中以不同的次序出現(很不幸那是合法的),也不會有任何影響。
(2)C++ 對”定義于不同編譯單元內的non-local static 對象”的初始化次序并無明確定義。為免除”跨編譯單元之初始化次序”問題,請以local static 對象替換non-local static 對象。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class FileSystem { ... }; FileSystem& tfs() //代替tfs對象 { ??static FileSystem fs; // 以local static的方式定義和初始化object ??return fs; // 返回一個引用 } class Directory { ... }; Directory::Directory( params ) { ??... ??std::size_t disks = tfs().numDisks(); ??... } Directory& tempDir() // 代替tempDir對象, { ??static Directory td; ??return td; } |
2. 構造/析構/賦值運算(Constructors,Destructors,and Assignment Operators)
條款05: 了解C++ 默默編寫并調用哪些函數
本條款告訴程序員,編譯器自動為你做了哪些事情。
用戶定義一個empty class (空類),當C++ 處理過它之后,如果你自己沒聲明,編譯器就會為它聲明(編譯器版本的)一個copy 構造函數、一個copy assignment操作符和一個析構函數。此外如果你沒有聲明任何構造函數,編譯器也會為你聲明一個default 構造函數。所有這些函數都是public 且inline 。舉例,如果你寫下:
| 1 | class Empty { }; |
這就好像你寫下這樣的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | class Empty { ?public: ??Empty() { ... } ??Empty(const Empty& rhs) { ... ) ??-Empty( ) { ... } ??Empty& operator=(const Empty& rhs) { ... } }; |
需要注意的是,只要你顯式地定義了一個構造函數(不管是有參構造函數還是無參構造函數),編譯器將不再為你創建default構造函數。
條款06: 若不想使用編譯器自動生成的函數,就該明確拒絕
本條款告訴程序員,如果某些對象是獨一無二的(比如房子),你應該禁用copy 構造函數或copy assignment 操作符,可選的方案有兩種:
(1) 定義一個公共基類,讓所有獨一無二的對象繼承它,具體如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Uncopyable { ?protected: //允許derived對象構造和析構 ??Uncopyable () {} ??-Uncopyable(} { } ?private: ??Uncopyable(const Uncopyable&}; //但阻止copying ??Uncopyable& operator=(const Uncopyable&); }; |
為阻止HomeForSale對象被拷貝,唯一需要做的就是繼承Uncopyable:
| 1 2 3 4 5 | class HomeForSale: private Uncopyable { ??… }; |
這種方法帶來的問題是,可能造成多重繼承,這回導致很多麻煩。
(2) 創建一個宏,并將之放到每一個獨一無二對象的private中,該宏為:
| 1 2 3 4 5 6 7 8 9 | // 禁止使用拷貝構造函數和 operator= 賦值操作的宏 // 應該類的 private: 中使用 #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ TypeName(const TypeName&); \ void operator=(const TypeName&) |
這種方法比第一種方法好,google C++編程規范中提倡使用該方法。
條款07: 為多態基類聲明virtual 析構函數
本條款闡述了一個程序員易犯的可能導致內存泄漏的錯誤,總結了兩個程序員應遵守的百編程原則:
(1)polymorphic (帶多態性質的) base classes 應該聲明一個virtual 析構函數。如果
class 帶有任何virtual 函數,它就應該擁有一個virtual 析構函數。這樣,但用戶delete基類指針時,會自動調用派生類的析構函數(而不是只調用基類的析構函數)。
(2)Classes 的設計目的如果不是作為base classes 使用,或不是為了具備多態性(polymorphically) ,就不該聲明virtual 析構函數。這是因為,當用戶將一個函數聲明為virtual時,C++編譯器會創建虛函數表以完成動態綁定功能,這將帶來時間和空間上的花銷。
條款08: 到讓異常逃離析構函數
(1)析構函數絕對不要吐出異常。如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然后吞下它們(不傳播)或結束程序。
(2)如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那么class 應該提供一個普通函數(而非在析構函數中)執行該操作。
條款09: 絕不在構造和析構過程中調用virtual 函數
條款10: 令operator= 返回一個reference to *this
本條款告訴程序員一個默認的法則:為了實現“連鎖賦值“,應令operator= 返回一個reference to *this。
條款11: 在operator= 中處理”自我賦值”
本條款討論了幾種編寫復制構造函數的正確方法。給出的結論是:確保當對象自我賦值時operator= 有良好行為。其中技術包括比較”來源對象”和”目標對象”的地址、精心周到的語句順序、以及 copy-and-swap。
(1) 復制構造函數的一種編寫方式如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | Widget& Widget::operator=(const Widget& rhs) { ??if (this == &rhs) return *this; //判斷是否為同一個對象,如果是自我復制,直接返回 ??delete pb; ??pb = new Bitmap(*rhs.pb); ??return *this; } |
這個版本存在異常方面的麻煩,即,如果”new Bitmap” 導致異常(不論是因為分配時內存不足或因為Bitmap 的copy構造函數拋出異常) , Widget 最終會持有一個指針指向被刪除的Bitmap 。
(2) 讓operator= 具備”異常安全性”往往自動獲得”自我賦值安全”的回報。因此愈來愈多人對”自我賦值”的處理態度是傾向不去管它,把焦點放在實現”異常安全性” (exception safety) 上,即:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | widget& Widget::operator=(const Widget& rhs) { ??Bitmap* pOrig = pb; ??pb = new Bitmap(*rhs.pb); ??delete pOrig; ??return *this; } |
如果”newBitmap” 拋出異常, pb (及其棲身的那個Widget) 保持原狀。即使沒有證同測試(identity test) ,這段代碼還是能夠處理自我賦值,但這種方法效率比較低。
(3) 另外一種比較高效的方法是:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Widget { ??…… ??void swap(Widget& rhs); //交換*this 和rhs 的數據:詳見條款29 ??…… }; Widget& Widget::operator=(Widget rhs) //rhs是被傳對象的一份復件(副本),注意這里是pass by value. { ??swap(rhs); //將*this 的數據和復件/副本的數據互換 ??return *this; } |
條款12: 復制對象時勿忘其每一個成分
本條款闡釋了復制對象時容易犯的一些錯誤,給出的教訓是:
(1) Copying 函數應該確保復制”對象內的所有成員變量”及”所有base class 成分”。
(2) 不要嘗試以某個copying 函數實現另一個copying 函數。應該將共同機能放進第三
個函數中,并由兩個coping 函數共同調。換句話說,如果你發現你的copy 構造函數和copy assignment 操作符有相近的代碼,消除重復代碼的做法是,建立一個新的成員函數給兩者調用。這樣的函數往往是private 而且常被命名為init。
3. 資源管理(Resource Management)
條款13: 以對象管理資源
本條款建議程序員使用對象管理資源(如申請的內存),給出的經驗是:
(1) 為防止資源泄漏,請使用RAII(“資源取得時機便是初始化時機” (Resource Acquisition Is Initialization; RAII))對象,它們在構造函數中獲得資源并在析構函數中釋放資源。
(2) 兩個常被使用的RAII classes 分別是trl: : shared_ptr 和auto_ptr。前者通常是較佳選擇,因為其copy行為比較直觀。若選擇auto_ptr,復制動作會使它(被復制物)指向null。
條款14: 在資源管理類中小心 copying 行為
本條款提醒程序員,使用資源管理類時需根據實際需要管理copying行為,常見的有:抑制copying、施行引用計數法。
條款15: 在資源管理類中提供對原始資源的訪問
(1) APIs往往要求訪問原始資源( raw resources) ,所以每一個RAII class 應該提供一個”取得其所管理之資源”的辦法。
(2) 對原始資源的訪問可能經由顯式轉換或隱式轉換。一般而言顯式轉換(如調用get()函數)比較安全,但隱式轉換對客戶比較方便。
條款16: 成對使用new 和delete 時要采取相同形式
本條款給出了程序員在申請和釋放資源時常犯的錯誤,給出的經驗是:
如果你在new 表達式中使用[],必須在相應的delete表達式中也使用[];如果你在new 表達式中不使用[],一定不要在相應的delete表達式中使用[]。
條款17: 以獨立語句將newed 對象置入智能指針
本條款指出了一個使用智能指針時常犯的錯誤,避免該錯誤可以這樣做:
以獨立語句將newed 對象存儲于(置入)智能指針內。如果不這樣做,一旦異常被拋出,有可能導致難以察覺的資源泄漏。舉例:
processWidget(std::trl::shared ptr<W工dget> (new Widget) , priority());
在調用processWidget之前,編譯器必須創建代碼,做以下三件事:
(1) 調用priority
(2) 執行”new Widget”
(3) 調用trl: : shared_ptr 構造函數
不同的C++ 編譯器執行這三條語句的順序不一樣,但對priority的調用可以排在第一或第二或第三執行。如果編譯器選擇以第二順位執行且priority函數拋出了異常,則新創建的對象Widget將導致內存泄漏,解決方法如下:
std::trl::shared_ptr<Widget> pw(new Widget); //在獨立語句內以智能指針存儲Widget對象
processWidget(pw, priority()); //這個調用肯定不存在內存泄漏
4. 設計與聲明(Designs and Declarations)
條款18: 讓接口容易被正確使用,不易被誤用
條款19: 設計class 猶如設計type
條款20: 提倡以pass-by -reference-to-const 替換pass-by-value
盡量以pass-by-reference-to- const 替換pass-by-value。 前者通常比較高效,并可避免
切割問題(slicing problem)(所謂切割問題,是指派生類的對象傳給基類類型的參數時,派生對象中的一些屬性會被截斷),需要注意的是,該規則并不適用于內置類型,以及STL 的迭代器和函數對象。對它們而言,pass-by-value 往往比較適當(實際上,STL中的迭代器和函數對象只支持值傳遞)。
條款21: 必須返回對象時,別妄想返回其reference
本條款告誡程序員:絕不要返回pointer 或reference指向一個local stack 對象,或返回reference 指向一個heap-allocated對象,或返回pointer 或reference指向一個local static 對象而有可能同時需要多個這樣的對象。
下面一一舉例說明。
(1) 如果返回pointer 或reference指向一個local stack 對象:
| 1 2 3 4 5 6 7 | const Rational& operator* (const Rational& lhs,const Rational& rhs) { ??Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //警告!糟糕的代碼! ??return result; } |
解釋:result是local對象,而local 對象在函數退出前被銷毀,這導致返回值墜入”無定義行為”。
(2) 返回reference 指向一個heap-allocated對象
| 1 2 3 4 5 6 7 | const Rational& operator* (const Rational& lhs,const Rational& rhs) { ??Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); ??return *result; } |
這種方式很容易造成內存泄露,如:
| 1 2 3 | Rational w, x, y , z; w = x * y * z; //與operator*(operator*(x, y) , z) 相同,內存泄露 |
(3) 返回pointer 或reference指向一個local static
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const Rational& operator* (const Rational& lhs, const Rational& rhs) { ??static Rational result; ??result = ... ; ??return result; } if((a * b) == (c * d)) { ??//當乘積相等時,做適當的相應動作; } else { ??//當乘積不等時,做適當的相應動作; } |
這樣做的問題是,(a * b) == (c * d)永遠為true。
條款22: 將成員變量聲明為private
條款23: 寧以non-member 、non-friend 替換member 函數
條款24 :若所有參數皆需類型轉換,請為此采用non-member 函數
當類的構造函數(未聲明為explicit)中包含參數時,該參數類型的對象或者數可隱式轉換為該對象。如果多個這樣的對象之間進行加減乘除,且要讓他們全部進行類型轉換,需要定義non-member函數(如友元函數)。
條款25:考慮寫出一個不拋異常的swap函數
5. 實現(Implementations)
條款26 :盡可能延后變量定義式的出現時間
本條款告訴程序員,如果你定義了一個變量且該類型帶一個構造函數或析構函數,當程序到達該變量時,你要承受構造成本,而離開作用域時,你要承受析構成本。為了減少這個成本,最好盡可能延后變量定義式的出現時間。
舉例說明:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 此函數太早定義了變量"encrypted" string encryptPassword(const string& password) { ??string encrypted; ??if (password.length() < MINIMUM_PASSWORD_LENGTH) { ???throw logic_error("Password is too short"); ?} ?//進行必要的操作,將口令的加密版本放進encrypted之中; ??return encrypted; } |
如果該函數拋出異常,變量encrypted便不會被使用。較好的做法是將變量”encrypted”的定義放到要用它的前一句或者能夠給它初值實參。
條款27:盡量少做轉型
本條款論證了為什么要盡量少做類型轉換,并告訴讀者,如果必須要進行類型轉換,有哪些注意事項。
常見的有三種類型轉換方式:
(1) C風格:(T)expression
(2) 函數風格:T(expression)
(3) C++ style cast
[1] const_cast<T>(expression) : 移除變量的const屬性
[2] dynamic_cast<T>(expression) : 安全向下轉型,即:基類指針/引用到派生類指針/引用的轉換。如果源和目標類型沒有繼承/被繼承關系,編譯器會報錯;否則必須在代碼里判斷返回值是否為NULL來確認轉換是否成功。
[3] reinterpret_cast<T>(expression):底層轉換
[4] static_cast<T>(expression):強迫隱式轉換,如,將non-const對象轉換為const對象,將int轉換為double類型。
對于這幾種類型轉換,給出的建議是“
(1) 如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_cast
(2) 寧可使用C++ style轉型,不要使用舊式轉型。前者容易識別出來,而且也比較有分門別類的指掌。
條款28 :避免返回handles指向對象內部成分
本條款告誡程序員,不要在類方法中返回handles(包括references、指針、迭代器)指向對象內部成分,因為這很容易導致空懸、虛吊(dangling)的對象。
條款29 :為“異常安全“努力是值得的
條款30 :透徹了解inlining的里里外外
Inline函數可免除函數調用成本,提高程序執行效率,但它也會帶來負面影響:(1)增大目標代碼的大小,有時候會非常龐大,需要動用虛存,這將大大降低程序執行速度。 (2) inline 函數無法隨著程序庫的升級而升級。換句話說如果f 是程序庫內的一個inline 函數,客戶將”f 函數本體”編進其程序中,一旦程序庫設計者決定改變f ,所有用到f 的客戶端程序都必須重新編譯。總之,將大多數inlining 限制在小型、被頻繁調用的函數身上才是最明智的選擇(根據80-20經驗準則,80%的時間花在20%的函數上)。
條款31: 將文件間的編譯依存關系降至最低
本條款介紹了降低文件間編譯依存關系的幾種方法。
常見的方法有兩種:Handle class和Interface class.
(1) Handle class. main class內含一個指針成員,指向其實現類。這般設計常被稱為pimpl idiom (pimpl 是”pointer to implementation” 的縮寫,這種class稱為“Handle class”。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include "Person.h" #include "PersonImpl.h" //我們也必須#include PersonImpl的class 定義式,否則無法調用其成員函數:注意, PersonImpl 有著和Person完全相同的成員函數,兩者接口完全相同。 Person::Person(const std::string& name , const Date& birthday, const Address& addr) : pImpl( new PersonImpl(name, birthday,? addr)) {} std::string Person;;name() const { ??return p Impl->name( ); } |
(2) Interface class. 實際上就是抽象基類
原創文章,轉載請注明:?轉載自董的博客
本文鏈接地址:?http://dongxicheng.org/cpp/effective-cpp-part1/
總結
以上是生活随笔為你收集整理的《Effective C++》读书笔记(第一部分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Effective STL》学习笔记(
- 下一篇: 《Effective C++》读书笔记(