#@date: 2014-06-16 #@author: gerui #@email: forgerui@gmail.com
前幾天買了好幾本書,其中有一本是《Effective C++》,準備好好學習一下C++.書中提出了55條應該遵循的條款,下面將逐一學習。點擊查看Evernote原文。c++
將C++分爲4個次語言。即:C, Objective-Oriented C++, Template C++, STL.數據庫
const,enum,inline
替換 #define
寧肯以編譯器替換預處理器:
1) 預處理器`#define N 1.653' 將全部出現N的地方替換爲1.653,當出現錯誤報的是1.653致使目標有問題,而不是N。若是使用變量,則可輕易地判斷。此外,替換會形成代碼在多處出現,增長代碼量。因此儘可能定義爲常量,const double N = 1.653;
2) 若是在數組初始化的時候,編譯器須要知道數組的大小,這樣,不可使用變量進行數組初始化,這時#define能夠,但咱們最好使用enum{N=3;}來替代define.
3) 使用#define定義一個三目運算符也會產生問題,若是你想得到高效,建議使用inline內聯函數。
但#include,以及#ifdef/#ifndef都是必需的,但咱們要儘可能限制預處理器的使用。express
const
1) const
表示不能夠改變,若是修飾變量,則表示這個變量不可變,如(a);若是修飾指針,表示指針指向的位置不可改變,如(b)。數組
const char * p = "hello"; //(a) *p的hello不可變, 與char const * p = "hello"等價 char * const p = "hello"; //(b) 表示p的值不可變,即p不能指向其它位置
2) STL迭代器的const安全
std::vector<int> vec; const std::vector<int>::iterator iter = vec.begin(); //相似T* const *iter = 10; //沒問題,改變iter所指物 ++iter; //錯誤!iter是const std::vector<int>const_iterator cIter = vec.begin(); //相似const T* *iter = 10; //錯誤,*iter是const iter++; //沒問題,能夠改變iter
3) 使函數返回一個常量值,能夠避免意外錯誤。以下代碼,錯把==
寫成=
,通常程序對*
號以後進行賦值會報錯,但在自定義操做符面前不會(由於自定義*
號後返回的是Rational對象實例的引用,能夠拿來賦值,不會報錯)。若是*
不寫成const
,則下面的程序徹底能夠經過,但寫成const以後,再對const進行賦值就出現問題了。數據結構
class Rational { ... }; const Rational operator* (const Rational& lhs, const Rational& rhs); Rational a, b, c; if(a * b = c); //把==錯寫成=,比較變成了賦值
4) 函數的參數,若是無需改變其值,儘可能使用const
,這樣能夠避免函數中錯誤地將==
等於符號誤寫爲=
賦值符號,而沒法察覺。多線程
5) const
做用於成員函數,兩個做用,a)能夠知道哪些函數能夠改變成員變量,哪些函數不能夠;b)改善C++效率,經過reference_to_const(即const對象的引用)方式傳遞對象。下面是常量函數與很是量函數的形式:app
class TextBlock{ public: ... const char& operator[] (std:size_t position) const{ return text[position]; } char& operator[] (std:size_t position) { return text[position]; } private: std::string text; }; /** *使用operator[] */ TextBlock tb("hello"); //non-const 對象 cout<<tb[0]<<endl; //調用的是non-const TextBlock::operator[] tb[0] = 'x'; //沒問題,寫一個non-const對象 const TextBlock cTb("hello"); //const 對象 cout<<cTb[0]<<endl; //調用的是const TextBlock:operator[] cTb[0] = 'x'; //錯誤,寫一個const對象
6) bitwise const主張const成員函數不能夠改變對象內任何non-static成員變量;logical const主張成員函數能夠修改它所處理的對象內的某些bits,但要在客戶端偵測不出的狀況下才得如此。編譯器默認執行bitwise。若是想要在const函數中修改non-static變量,需將變量聲明爲mutable
(可變的)。函數
class TextBlock{ private: char* pText; mutable std::size_t textLength; mutable bool lengthIsValid; public: ... std::size_t length() const; }; std::size_t TextBlock::length() const{ if (!lengthIsValid){ textLength = std::strlen(pText); //加上mutable修飾後,即可以修改其值 lengthIsValid = true; } }
7) 避免const和non-const成員函數重複學習
思想很簡單,若是const和non-const成員函數功能至關時,就用non-const函數去調用const函數(不能反過來...o_O
)。
class TextBlock{ public: const char& operator[](std:size_t position) constP ... return text[position]; } char& operator[] (std:size_t position){ return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]); } };
1) 對內置類型(基本類型)手動進行初始化。
int x = 0; const char* p = "Hello World"; double d; std:cin >> d;
2) 內置類型之外的類型,初始化要靠構造函數。類的構造函數使用成員初值列(member initialization list),而不是在構造函數中進行賦值操做。初值列成員變量的排列順序與其聲明順序相同。
class PhoneNumber { ... }; class ABEntry { 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 numberTimesConsulted; }; ABEntry::ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones){ theName = name; //這些都是賦值,而非初始化 theAddress = address; thePhones = phones; numberTimesConsulted = 0; } /** *使用成員初值列,效率更高 */ ABEntry::ABEntry(const std:string& name, const std::string& address, const std:list<PhoneNumber>& phones) :theName(name), theAddress(address), thePones(phones), numberTimesConsulted(0) //成員初值列 { ... }
3) 爲避免"跨編譯單元之初始化次序"問題,以local static對象替換non-local static對象。
//FileSystem源文件 class FileSystem{ public: ... std::size_t numDisks() const; };
extern FileSystem tfs; //Directory源文件,與FileSystem處於不一樣的編譯單元 class Directory{ public: Directory(params); ... }; Directory::Directory(params){ ... //調用未初始化的tfs會出現錯誤 std::size_t disks = tfs.numDisks(); }
這樣的話,Directory類會調用一個non-local的tfs,而這個tfs未必經歷了初始化處理。咱們要有效避免這個狀況,使獲取的tfs對象保證是初始的,可使用以下的一個函數獲取,這就像Singleton(單例)模式同樣。
class FileSystem { ... }; FileSystem& tfs(){ static FileSystem fs; return fs; } class Directory { ... }; Directory::Directory(params){ std::size_t disks = tfs().numberDisks(); } Directory& tempDir(){ static Directory td; return td; }
通過上面的處理,將non-local轉換了local對象,這樣作的原理是:函數內的local static 對象會在"該函數被調用期間","首次趕上該對象之定義式"時被初始化,這樣就保證了對象被初始化。這樣作的好處是不調用函數時,不會產生對象的構造和析構。但對多線程這樣的方法會有問題。
1) 編譯器會自動爲class建立default構造函數、copy構造函數、copy assignment操做符、以及析構函數。
2) 若是用戶聲明瞭一個構造函數,則編譯器則不會再爲它聲明default構造函數。
3) 拷貝構造函數能夠經過=
或()
實現。默認的拷貝構造函數對指針進行地址的複製,這樣會產生多個對象共用一塊地址,會產生問題,能夠本身實現拷貝構造函數,實現值的複製。
1) 不容許用戶進行對象的拷貝。通常編譯器會提供默認拷貝,可將相應的成員函數聲明爲private而且不予以實現。但這是有個問題,member(成員)函數和friend(友元)函數仍然能夠調用。
2) 在不想實現的函數中不寫函數參數的名稱。
class HomeForSale{ public: ... private: HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&); };
3) 將錯誤移至編譯期,更早地發現錯誤每每更好。定義一個Uncopyable的基類,其它類繼承該類,當執行拷貝時,要調用基類拷貝構造函數,就會出現問題。
class Uncopyable{ protected: Uncopyable(); ~Uncopyable(); private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); };
1) polymorphic(帶多態性質的)base classes 應該聲明一個virtual析構函數。這樣,每一個派生類都要實現析構函數,防止指向derived classes的對象沒有析構函數。
2) 若是class帶有任何virtual函數,它就應該擁有一個virtual析構函數。
3) Classes的設計目的若是不是做爲base classes使用,或不是爲了具有多態性,就不應聲明virtual析構函數。
4) 含有純虛函數的類是抽象類。
class AWOV{ public: virtual ~AWOV() = 0; //純虛函數 };
5) 不是全部類都是被設計做爲基類來使用的。如string類和STL容器類,因此這些類不須要聲明爲virtual。
1) 析構函數絕對不要吐出異常。若是析構函數調用的函數可能拋出異常,析構函數應該捕捉異常,而後吞下它們或結束程序。
class DBConnection{ public: static DBConnection create(); void close(); }; class DBManager{ public: ~DBManager(){ db.close(); //析構函數關閉數據庫鏈接 } private: DBConnection db; }; //調用析構函數時可能會發生異常 DBManager dbM(DBConnection::create());
上面的代碼爲了幫助忘記關閉數據庫鏈接的客戶關閉鏈接,在析構函數中調用了close函數,但這個函數可能出現異常,這種必須調用可能產生異常的函數時,須要進行異常捕獲。以下:
DBManager::~DBManager(){ try { db.close(); } catch(...){ //能夠記錄錯誤後退出程序 std::abort(); } }
2) 上面這個問題還不是完善的方案,即便析構函數捕獲到異常,客戶也沒法處理異常,客戶須要對某個函數運行期間拋出的異常進行反應,那麼class應該提供一個普通函數來執行該操做。
class DBManager(){ public: void close(){ db.close(); closed = true; } ~DBManager(){ if (!closed){ try { db.close(); } catch(...) { //錯誤日誌... } } } private: bool closed; DBConnection db; };
這裏面加了一個close函數,客戶能夠本身調用close函數,當發生異常時,進行異常處理。若是客戶沒有調用close函數,則能夠在析構函數中自動調用。因此,在寫程序時,必定要將會發生異常的函數做爲一個普通函數,這樣能夠提供更多的選擇。
1) 在構造和析構函數期間不要調用virtual函數,由於這類調用從不降低到derived class(子類)。父類的構造函數先於子類執行,因此父類的自身成分早於子類構造,子類的virtual函數尚未生成,因此即便調用virtual函數,也只會調用父類的virtual函數,即這個被聲明爲virtual的函數在構造函數中毫無心義。
reference to *this
1) 令賦值(assignment)操做符返回一個reference to *this
。這樣就能夠像基本式同樣連續賦值,如基本式的連續賦值:int a,b,c; a=b=c=1
。
class Widget{ public: Widget& operator+=(const Widget& src){ ... return *this; } Widget& operator=(const Widget& src){ ... return *this; } }
1) 確保當對象自我賦值時,operator=有良好行爲。其中技術包括比較「來源對象」和「目標對象」的地址、精心周到的語句順序、以及copy-and-swap。
2) 肯定任何函數若是操做一個以上的對象,而其中多個對象是同一個對象時,其行爲仍然正確。
1) Copying函數應該確保複製「對象內的全部成員變量」及「全部base成分」。
2) 不要嘗試以某個copying函數實現另外一個copying函數。應該將共同機能放進第三個函數中,並由兩個copying函數共同調用。
1) 防止資源泄漏,請使用RAII(Resource Acquisition is Initialization;資源取得時機即是初始化時機)對象,它們在構造函數中得到資源並在析構函數中釋放資源。
2) 兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者一般是較佳選擇,由於其copy行爲比較直觀。若選擇auto_ptr,複製動做會使它(被複制物)指向null,即只有一個對象指向這個資源。
1) 複製RAII對象必須一併複製它所管理的資源,因此資源的copying行爲決定RAII對象的copying行爲。 2) 廣泛而常見的RAII class copying行爲是:抑制copying、施行引用計數法。不過其餘行爲也均可能被實現。
1) APIs每每要求訪問原始資源,因此每個RAII class應該提供一個「取得其所管理的資源」的方法。 2) 對原始資源的訪問可能經由顯式轉換或隱式轉換。顯式轉換更安全,隱式轉換更方便。
1) 若是new數組時使用[],那麼釋放資源時就要用delete[],這會調用多個析構函數去釋放資源;若是使用new對象不使用[],釋放時必定不要使用[]。保持二者一致。
std::string str = new std::string; std::string strArr = new std::string[20]; //釋放資源 delete str; delete[] strArr;
1) 以獨立語句將newed對象存儲於(置入)智能指針內。若是不這樣作,一旦異常被拋出,很難察覺到資源泄漏。
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
上面的代碼存在須要三個步驟:
priority()
new Widget
tr1::shared_ptr
構造函數但C++的編譯器對這三個執行的次序並不固定,而Java和C#則以特定的順序完成。但C++中能夠肯定的是,new Widget
必定比tr1::shared_pt
r先執行,但對priority()函數的調用卻沒有限定。若是如下面的順序:
new Widget
priority()
函數tr1::shared_ptr
構造函數這就會引起一個問題,若是第二步priority()
函數發生異常,那麼new Widget就沒法放入shared_ptr
中,這樣就會形成資源泄漏(shared_ptr
用來進行資源管理)。正確的作法是將語句分離,先建立資源並放到資源管理器後,再進行下步操做。
//先建立對象並置入資源管理器中 std::tr1::shared_ptr<Widget> pw(new Widget); //再進行下步操做 processWidget(pw, priority);
1) 好的接口容易被正確使用。 2) 保持接口的一致性,與內置類型行爲兼容。 3) 爲阻止誤用,能夠採用創建新類型、限制類型上的操做,束縛對象值,消除客戶的資源管理責任。 4) tr1::shared_ptr支持定製型刪除器。這可防範DLL問題,可被用來自動解除互斥鎖。
1) class的設計就是type的設計。在定義一個新type以前,請肯定符合一些規範。
1) 儘可能以pass-by-reference-to-const替換pass-by-value。前者一般比較高效,並可避免切割問題。 2) 以上規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value每每比較適當。
1) 毫不要返回pointer或reference指向一個local stack對象,或返回reference指向一個heap-allocated對象,或返回pointer或reference指向local static對象而有可能同時須要多個這樣的對象。
1) 切記將成員變量聲明爲private。這樣能夠保證數據的一致性、可細微劃分方向控制、允諾條件得到保證,並提供class做者以充分的實現彈性。 2) protected並不比public更具封裝性。
1) 寧肯拿non-member non-friend函數替換member函數。這樣能夠增長封裝性、包裹彈性和機能擴充性。
1) 若是你須要爲某個函數的全部參數(包括被this指針所指的那個隱喻參數)進行類型轉換,那麼這個函數必須是個non-member。
1) 當std::swap效率不高時,能夠提供本身版本的swap,但要保證這個swap不會拋出異常。 2) 若是提供一個member swap,也應該提供一個non-member swap來調用前者。 3) 調用swap時,使用using std::swap;
聲明std::swap,而後調用swap而且不加任何「命名空間修飾符」。 4) 爲「用戶定義類型」進行std template特化是好的,但不要嘗試在std內加入某些對std而言是全新的東西。
1) 儘量延後變量定義的時間。這樣能夠增長程序的清晰度並改善程序效率。
std::string encryptPass(string& pass){ using namespace std; //在拋出異常前定義,若是拋出了異常,則沒有必要定義這個變量 string encrypted; if (pass.length() < MinLenth){ throw login_error("Password is too short"); } //應該把變量移動這裏 //string encrypted; ... return encrypted; }
1) 兩個舊式轉型。 1. (T)expression 2. T(expression) 2) 四個新式轉型。 1. const_cast
: 將const轉爲non-cast。 2. dynamic_cast
: 將父類轉爲子類(耗費重大,循環中儘可能不要用)。 3. reinterpret_cast
: 執行低級轉型,根據編譯器不一樣有所改變,不能夠移植。(不多用)。 4. static_cast
: 作上面三個轉型的逆操做。 3) 若是能夠,儘可能避免轉型,特別是在注重效率的代碼避免使用dynamic_casts
。可使用virtual的繼承去實現-_1!
。 4) @^@
若是能夠將轉型放在函數背後,客戶能夠調用該函數,而不須要進行轉型操做。 5) 寧肯使用新式(C++-style)轉型,不要使用舊式轉型。前者更明確,更容易查找。
1) 避免返回handles(包括指針、reference、迭代器)指向對象內部。這能夠增長封裝性,幫助const成員函數的行爲像個const,並將發生「虛吊號碼」的可能性降至最低。
1) 異常安全函數(Exception-safe functions)即便發生異常也不會泄漏資源或容許任何數據結構破壞。這樣的函數區分爲三種可能的保證:基本型、強烈型、不拋異常型。 2) 「強烈保證」每每可以以copy-and-swap
實現,但要考慮資源消耗和效率問題,不是全部狀況都有必要的。 3) 函數提供的「異常安全保證」一般最高只等於其所調用之各個函數的「異常安全保證」中的最弱者。