條款一:視c++為一個語言聯邦
c++可以認為是由C,Object-Oriented C++(面向對象),Template C++(模板),STL(c++標准模板庫)四種次語言組成的。
條款二:盡量以const,enum,inline替換#define
c++中推薦使用其他的方法替換一些宏定義操作,如常量定義,推薦使用const int MAX = 10 替換#define MAX 10。而對於#define定義的函數,也推薦使用inline(內聯函數)替換。另外如在類中要聲明成員屬性的值。必須聲明成靜態常量,或者使用enum。如
class Stack{ public: ... private: static const int size = 10; // 或者 enum { size = 10 }; int members[size]; }
條款三:盡可能使用const
const在c++里面可以說是用的非常多的一個修飾詞,const可以修飾值,指針,函數,函數參數,函數返回值等。
如const char* p = "hello"; 指針指向的值為常量。char* const p = "hello";指向值的指針為常量。可以記為const在*左邊值為常量,const在*右邊指針為常量。如果*兩邊都有const,則值和指針都為常量。
當你在使用stl容器時,使用迭代器訪問容器中的元素,而又不希望通過迭代器修改容器中的值,可以使用const_iterator,或者是遍歷常量容器對象,使用const_iterator。
const修飾成員函數,該成員函數不可以修改成員屬性值,另外const常用來傳遞的引用參數,當你不希望修改引用參數中的內容時,可以用const修飾。
條款四:確定對象被使用前已先被初始化
為內置對象進行手工初始化,因為c++不保證初始化它們,即你想初始化一個int對象,最好將int x;改寫成int x = 0;當然大部分情況下c++會為int x;分配 x = 0。
構造函數初始化成員變量時,最好使用成員初值列,這樣效率比賦值要高一些,尤其是對非內置對象,內置對象兩者沒影響。如
//推薦使用 class Student{ public: Student(string& name, int age):name_(name),age_(age) {}; private: string name_; int age_; } //不推薦 class Student{ public: Student(string& name, int age) { name_ = name; age_ = age; }; private: string name_; int age_; }
上面第一種只有對於string對象只有copy操作,而第二種既有copy操作,還有賦值操作。
條款五:了解c++默默編寫並調用哪些函數
你定義一個空類,編譯器會為它分配default構造函數,copy構造函數,copy assignment操作符合一個析構函數。如
class Empty {};
實際上上面的類等價於
class Empty { public: Empty() {...}; Empty (const Empty& e) {...}; ~Empty() {...}; Empty& operator=(const Empty& e) {...}; }
如果你自己定義了構造函數,copy構造函數,copy assignment,析構函數,則編譯器不會再分配這些函數。
編譯器分配的copy assignment有時候存在問題,如string& name這樣的成員屬性,編譯器分配的copy assignment是不可以賦值的,你需要自己重新定義copy assignment。
條款六:若不想使用編譯器自動生成的函數,就該明確拒絕
如條款五所說編譯器會自動分配如copy構造函數,copy assignment這類的函數,如果你不想要這些函數,可以在private下聲明這些函數就行了。如
class Student{ private: Student (const Student&); Student& operator=(const Student&); }
條款七:為多態基類聲明virtual析構函數
帶有多態性質的基類,或者類中有帶vitural的成員函數,則其析構函數都應該定義為virtual析構函數。但如果該類不是設計為基類,則不要聲明virtual析構函數。如果基類中的析構函數沒有定義為vitural,則會出現一些問題,如在工廠模式中。有以下代碼
class Person{ public: Person(); ~Person(); } class Student : public Person {...}; Person* getPerson(); Person* student = getPerson();
delete student;
假設當前的getPerson()獲取一個派生類Student對象,當使用完之后delete student時因為指針是基類指針,所以會銷毀基類中的成員,但是不會銷毀派生類中的成員,所以會存在”部分銷毀“的現象,但是若基類中的析構函數是vitural,則會刪除派生類中的成員。
另外如果一個類不是基類,不要聲明vitural函數,因為vitural會引入虛表指針和虛表,會占用一部分內存。
條款八:別讓異常逃離析構函數
這種通常指在析構函數中調用了一些函數,而這些函數可能引入異常,此時需要做一些處理,盡量保證不要讓析構函數的異常傳遞出來,或者說盡量確保析構函數中不要發生異常。
條款九:絕對不要在構造函數和析構函數中調用virtual函數
因為上述這類調用絕對不會下降至下一層,即派生層,這種情況避免就好了。一般也不會這樣去調用
條款十:令operator= 返回一個reference to *this
這是一種固定的assignment 操作符寫法,如
class Student{ public: Student& operator=(const Student& s){ name_ = s.name_; age_ = s.age_; return *this; } private: string name_; int age_; }
除了operator= 如operator+=等都可以寫成這樣。
條款十一:在operator= 中處理”自我賦值“
自我賦值,即a = a;當然一般這種情況不會發生,但是這種*a = *b,而指針a和b都指向同一個值,這樣就存在問題。如果operator= 是下面這種寫法
class Bitmap {}; class Widget { public: Widget& operator=(const Widget& rhs){ delete pb; pb = new Bitmap(*rhs.pb); return *this; } private: Bitmap* pb; }
如果rhs == this,則delete pb時也刪除了rhs中的pb。所以可以改寫成
class Bitmap {}; class Widget { public: Widget& operator=(const Widget& rhs){ Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; } private: Bitmap* pb; }
條款十二:復制對象時勿忘其每一個成分
copy構造函數和copy assignment函數中不要忘了每一個成員變量,忘記了編譯器也不會報錯。如
class Student{ public: Student (const Student& s):name_(s.name_) {}; private: string name_; int age_; }
派生類中的copy構造函數也不要忘了基類中的成員變量,可以直接調用基類的構造函數。
class Person{ public: Person (const Person& p):name_(p.name_), age_(p.age_) {}; private: string name_; int age_; } class Student : public Person { public: Student (const Student& s) : Person(s), grade_(s.grade_){}; private: int grade_; }
條款十三:以對象管理資源
在c++中new和delete必須是同時存在的,但很多時候會忘記delete,或者說你很仔細的沒有忘記delete,但是在new和delete之間的代碼可能會存在return,continue等這類操作,而跳過了delete,為了防止這種內存泄漏的情況發生,所以也就有了以對象管理資源,當對象唄釋放時,對象的析構函數會自動釋放這些資源,如auto_ptr就是這種的資源管理對象。而c++11中的unique_ptr,shared_ptr也是這一類對象,只不過unique_ptr不能多個對象共享一塊內存,而shared_ptr通過引用計數機制,可以多個對象共享一塊內存,只有當引用計數為0才會釋放內存。
條款十四:在資源管理類中小心copying行為
RAII(Resource Acquisition Is Initailization,資源取得時便是初始化時,也是以對象管理資源的概念)對象復制時要一並復制它所管理的資源。而普遍常見的RAII class copying行為是:抑制copying,施行引用計數法等。
條款十五:在資源管理類中提供對原始資源的訪問
APIs中往往需要訪問原始資源,所以每一個RAII class應該提供一個”取得其所管理之資源“的方法,如get成員函數可以獲得原始指針,重載了指針取值運算符,轉換到原始指針並取值。
對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯式轉換比較安全,但隱式轉換對客戶比較方便。
條款十六:成對使用new和delete時要采取相同形式
如下例子
string* stringArray = new string[100]; ... delete stringArray;
上面的new和delete並不是一個很好的應用,因為new出來的是一個數組,而delete很可能只釋放了一個元素,標准的用法是
string* stringArray = new string[100]; ... delete [] stringArray;
所以說new和delete,new [] 和delete []。針對此問題要注意typedef的使用,最好不要對數組使用typedef,否則很容易在delete時忘了帶[]。
條款十七:以獨立語句將newed對象置入智能指針
以獨立語句將newed對象存儲於智能指針內。如果不這樣做,一旦異常拋出,有可能導致難以察覺的資源泄漏。如
int priority(); void processWidget(std::shared_ptr<Widget> pw, int priority); //調用 processWidget(std::shared_ptr<Widget> (new Widget), priority());
在c++中上述執行的順序並不嚴格,可能先執行new Widget,再執行priority(),最后才執行shared_ptr的調用。這樣一旦priority()調用報錯,就不會發生new Widget的內存泄漏。所以最好獨立語句將對象放入智能指針。
std::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
條款十八:讓接口容易被正確使用,不易被誤用
總結成一句話就是在設計接口時,盡量簡潔明了,約束性強,不容易發生誤用。
條款十九:設計class猶如設計type
條款二十:寧以pass-by-reference-to-const替換pass-by-value
在函數的參數傳遞過程中對於非內置類型對象,盡量以引用或指針傳遞,推薦引用,為了避免函數中修改傳遞的對象,可以加上const修飾符。對於非內置類型,引用的傳遞要比傳值高效很多,因為傳值的過程中相當於傳遞副本,是需要調用copy構造函數,而函數執行完之后還會調用析構函數銷毀copy的對象,而引用傳遞不存在這一問題。
條款二十一:必須返回對象時,別妄想返回其reference
在函數的返回值時切勿返回reference、pointer,尤其是指向函數中的局部對象,直接返回值即可,雖然這樣會耗時(調用構造函數),但至少是正確的。
條款二十二:將成員變量聲明為private
條款二十三:寧以non-member,non-friend替換member函數
條款二十四:若所有參數皆需類型轉換,請為此采用non-member函數
條款二十五:考慮寫出一個不拋異常的swap函數
條款二十六:盡可能延后變量定義式的出現時間
定義的變量即使沒有被使用,也會存在構造和析構的操作,也就是或會存在構造和析構的成本,所以在定義變量時盡量在使用時定義。
條款二十七:盡量少做轉型動作
如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_casts。
如果轉型是必要的,試着將他隱藏於某個函數背后。客戶隨后可以調用該函數,而不需將轉型放入到他們自己的代碼內。
寧可使用c++風格的新式轉型,不要使用舊式轉型。
條款二十八:避免返回handles指向對象內部成分
對於下面的例子,返回引用(handles,指針,迭代器也可以歸為這一類)會導致內部數據的不安全.
class Point { public: Point(int x, int y); void setX(int newVal); void setY(int newVal); } struct RectData{ Point ulhc; Point lrhc; } class Rectangle{ public: Point& upperLeft () const {return pData->ulhc}; Point& lowerRight () const {return pData->lrhc}; private: std::shared_ptr<RectData> pData; }
上面的Rectangle類中,雖然upperLeft函數是const函數,但是返回的結果是Point的引用,通過該引用是可以修改Point內部的值,包括返回指針和迭代器都是會發生類似的情況,所以如果硬要返回引用這類handles,可以返回const references
class Rectangle{ public: const Point& upperLeft () const {return pData->ulhc}; const Point& lowerRight () const {return pData->lrhc}; private: std::shared_ptr<RectData> pData; }
條款二十九:為”異常安全“而努力是值得的
條款三十:透徹了解inlining的里里外外
總之只有在小型且頻繁被調用的函數身上才使用inlining。這樣可以保證代碼膨脹的問題最小化,程序的速度提升最大化,如max函數。
條款三十一:將文件間的編譯依存關系降至最低
條款三十二:確定你的public繼承塑模出is-a關系
基類和派生類之間一定要有is-a關系,即基類的屬性和方法,在派生類中一定是合理存在的。
條款三十三:避免遮掩繼承而來的名稱
只要熟悉作用域,根據作用域去定義變量就可以了,知道在使用變量時作用域的先后順序。
條款三十四:區分接口繼承和實現繼承
這里涉及到基類中的純虛函數,虛函數和非虛函數以及繼承的概念。
純虛函數:含有純虛函數的類是抽象類,不可以被new出來。對於純虛函數,子類只繼承了接口,子類中必須聲明和實現純虛函數,當然父類中也可以定義純虛函數,但要指定父類名稱才可以調用。
虛函數:子類會繼承虛函數的接口和缺省實現,子類中也可以重寫虛函數。
非虛函數:子類會繼承非虛函數的接口和強制實現,不建議子類中重寫非虛函數。如果硬要重寫,調用時基類指針會調用基類中的函數,想調用子類中的重寫函數,必須使用子類指針。
條款三十五:考慮virtual函數以外的其他選擇
在子類繼承父類的功能時,可以引入一些設計模式來使得繼承更靈活。如使用模板模式,策略模式。
條款三十六:絕不重新定義繼承而來的非虛函數
正如上面所說如果子類重寫了父類的非虛函數,父類指針即使是指向子類,當調用非虛函數時,還是會調用父類的非虛函數,所以非虛函數的調用和指針類型綁定。所以為了避免出現這樣的迷惑行為,還是不要重寫非虛函數。
條款三十七:絕不重新定義繼承而來的缺省參數值
對於繼承而來的虛函數或純虛函數,如果函數中有定義的缺省參數值,繼承時不要修改缺省參數值,因為缺省參數值是靜態綁定(發生在編譯期)的。而虛函數或純虛函數時動態綁定(發生在運行期)的。
條款三十八:通過復合塑模出has-a或”根據某物實現出“
條款三十九:明智而審慎地使用private繼承
private並不會繼承接口,而是繼承基類的實現,總之盡量使用public,除非特殊實現上,因為private繼承本質上不屬於is-a關系。
條款四十:明智而審慎地使用多重繼承
總之能使用單一繼承的盡量使用單一繼承,多重繼承太過復雜,容易引入歧義,且應用於多重繼承的虛繼承既要提前設計也會引入大小、速度等成本。
條款四十一:了解隱式接口和編譯器多態
c++中的模板創造了隱式接口和編譯期的多態。
條款四十二:了解typename的雙重意義
在聲明模板時,template<typename T> 和 template<class T>的含義是一樣的,但是typename可以表示嵌套從屬類型,如T::setX到底是一個類型還是成員名稱,如果加上typename就很明確是一個類型了,typename T::setX。