Part1一、讓自己習慣C++
條款01:視C++為一個語言聯邦
C++是一個帶有一組守則的、一體的語言:他就像從四個次語言( C、Object-Oriented C++、Template、STL ) 組成的聯邦政府,每個次語言都有自己的規(guī)約。記住這四個次語言你就會發(fā)現C++容易了解得多。條款02:盡量以const,enum,inline替換 #define
#define ASPECT_RATIO 1.653
以上句為例,是通過預處理器處理而不是編譯器處理,有可能ASPECT_RATIO 沒進入記號表內,于是如果出現了編譯錯誤,那么編譯器會提示錯誤信息是 1.653 而不是 ASPECT_RATIO ,你會感到非常困惑。const double AspectRatio = 1.653這樣編譯器就可以看到ASPECT_RATIO ,而且使用常量會使代碼量較小,因為預處理器只會因盲目的替換而出現多份 1.653對于class的專屬常量,為了限制作用域在class內,并且防止產生多個實體,最好使用static 1. 如果你的編譯器支持在類內對const static 整數類型聲明時獲初值,則使用
2. 如果不支持,則在類內定義,在對應的實現文件中賦值
如果你需要在編譯器就使用一個class常量值,則應最好改用枚舉類型enum,并且枚舉不能用來取地址,不會為它分配額外的存儲空間對于形似函數的宏,最好改用inline的模板函數條款 03:盡可能使用const
const出現在星號左邊目標是指物是常量,出現在星號右邊表示指針本身是常量,如果出現在兩邊,則指針和物都是常量void f1(const Widget* pw)和void f2(Widget const* pw)兩種寫法意義相同,都表示被指物是常量
對于STL迭代器來說,如果你希望迭代器所指的東西不可改動,你需要的是const_iterator
令函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外(例如把一個值賦值給一個返回值)
將const實施與成員函數的目的是為了明確該成員函數可作用于const對象: 1. 他們使class接口比較容易理解
2. 他們使得可以操作const對象
const成員函數和no-const成員函數可重載,即可以同時出現,在傳入不同的參數時候會調用不同的版本,但是有時我們需要這樣,但是又不想代碼重復,我們可以在no-const成員調用const成員函數來處理這個代碼重復問題
例如:const_cast<char &>( static_cast<const TextBlock&>(*this)[position]),經過這樣操作里面先安全轉型使得調用的是const版本,外面再去const轉型條款 04:確定對象被使用前已先被初始化
對于內置類型要進行手工初始化,構造函數最好使用成員初值列表,不要在構造函數中使用賦值操作來初始化,而且初值列表列出的成員變量次序應該和在class中聲明的次序一樣,因為聲明次序就是C++保證的初始化次序
對于static對象,在跨編譯單元之間的初始化次序是不能確定的,因為C++只保證在本文件內使用之前一定被初始化了舉例(使用如下方式可以解決這個問題即以loacl static對象替換non-local static對象):class FileSystem{...};
FileSystem& tfs(){
static FileSystem fs;
return fs;
}
Part2二、構造/析構/賦值運算
條款05:了解C++默默編寫并調用了哪些函數
如果你不定義,編譯器會自動幫你實現默認的構造函數,析構函數,拷貝賦值運算符和拷貝構造函數,但是如下幾種情況不會替你生成默認的拷貝賦值運算符 1. 類中含有引用的成員變量
2. 類中含有const的成員變量
3. 類的基類中的拷貝賦值運算符是私有成員函數
條款06:若不想使用編譯器自動生成的函數,就應該明確拒絕
當我們不希望編譯器幫我們生成相應的成員函數的時候,我們可以將其聲明為private并且不予以實現條款07:為多態(tài)基類聲明virtual析構函數
1. 用來作為帶有多態(tài)性質的基類的類
2. 一個類中帶有任何virtual函數
如果類的設計目的不是作為基類使用,那么就不應該為它聲明virtual析構函數條款08:別讓異常逃離析構函數
析構函數不要拋出異常,如果實在要拋出異常,那么最好使用std::abort(),放在catch中,把這個行為壓下去
如果某個動作可能會拋出異常,那么最好把它放在普通函數中,而不是放在析構函數里面,讓客戶來執(zhí)行這個函數并去處理條款09:絕不再構造和析構函數中調用virtual函數
在構造和析構的時候,不要試圖調用或在調用的函數中調用virtual函數,因為會調用父類版本導致出現一些未定義的錯誤解決辦法之一:
class Transaction{
publci:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logIngo) const;//把它變成這樣的non-virtual函數
...
};
Transaction::Transaction(const std::string& logInfo){
...
logTransaction(logInfo);//這樣調用
}
class BuyTransaction: public Transaction{
BuyTransaction( parameters ):Transaction(createLogString( parameters )){...}//將log信息傳給基類的構造函數
private:
static std::string createLogString( parameters );//注意此函數為static函數
}
條款10:令operator= 返回一個reference to *this
為了實現連鎖賦值如內置類型x= y = z =15由于=采用右結合律,所以等價于x = (y = (z = 15)),因此,為了使我們自定義類也實現,所以*重載=,+=,-=,*=使其返回refercence to this條款11:在operator= 中處理“自我賦值”
在賦值的時候會出現對自我進行賦值的情況,這種情況下我們很容易寫出不安全的代碼Widget::operator=(const Widget& rhs){
delete pb; //把自己釋放了
pb = new Bitmap(*rhs.pb);//這就不安全了
return *this;
}
Widget::operator=(const Widget& rhs){
if(this == &rhs) return *this;//驗證是不是相同
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
Widget::operator=(const Widget& rhs){
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);//讓pb指向*pb的一個副本
delete pOrig; //刪除原先的pb
return *this;
}
class Widget{
...
void swap(Widget& rhs);//交換*this和rhs的數據
...
};
Widget::operator=(const Widget& rhs){
Widget temp(rhs);//創(chuàng)建一個rhs副本
swap(temp);//交換*this和上面的副本
return *this;
}
條款12:復制對象時勿忘其每一個成分
為了確保復制的時候復制對象內的所有成員變量,我們應該在子類的構造和賦值函數中調用父類的構造和賦值函數來完成各自的任務
不要嘗試在復制構造函數和賦值函數中相互調用,如果想消除重復代碼,請建立一個新的成員函數,并且最好將其設為私有且命名為initPart3三、資源管理
條款13:以對象管理資源
為了防止資源泄露,我們應該在構造函數中獲取資源,在析構函數中釋放資源,這樣可以有效的避免資源泄露
使用智能指針是一個好的辦法,在C++11中auto_ptr已經被棄用,有三個常用的是unique_ptr,share_ptr和weak_ptr條款14:在資源管理類中心copying行為
我們在管理RAII(構造函數中獲得,析構函數中釋放)觀念的類時,應該對不同的情況,根據不同的目的進行處理 1. 當我們處理不能同步擁有的資源的時候,可以才用禁止復制
2. 當我們希望共同擁有資源的時候,可以采用引用計數法
3. 當我們需要拷貝的時候,可以采用深拷貝
4. 或者某些時候我們可以采用轉移底部資源擁有權的方式
條款15:在資源管理類中提供對原始資源的訪問
有的api函數往往需要訪問類的原始資源,所以每一個RAII類應該提供一個返回其管理的原始資源的方法
返回原始資源可以使用顯示轉換也可以使用隱式轉換,但是往往顯示轉換更加安全一點,但是隱式轉換更加方便class Font{
...
FontHandle get() const {return f;} //顯示轉換
...
operator FontHandle() const {return f;} //隱式轉換函數
....
private:
FontHandle f; //管理的原始資源
}
條款16:成對使用new和delete時要采用相同形式
不要對數組形式做typedef,因為這樣會導致delete的時候調用的是delete ptr而不是delete [] ptr,對內置類型會出現未定義或有害的,對類的類型會導致無法調用剩余的析構函數,導致類中管理的資源無法釋放,從而造成內存泄漏
在new 表達式中使用[ ] ,則在相應的delete 表達式中也使用 [ ]條款17:以獨立語句將newed對象置入智能指針
諸如這樣的語句processWidget (std::tr1::shared_ptr(new Widget),priority())
1. 在先執(zhí)行new Widget`語句和調用std::tr1::shared_ptr構造函數之間
2. 不能確定priority函數的執(zhí)行順序,可能在最前面,也可能在他們的中間
Part4四、設計與聲明
條款18:讓接口容易被正確使用,不易被誤用
我們接口應該替客戶著想,考慮周全,避免它們犯錯誤。例如在向函數傳遞日期的時候,把日期參數做成類的形式,并且用static成員函數來返回固定的月份,避免用戶參數寫錯
接口應該和內置接口保持一致,避免讓客戶感覺不舒服,這方面STL做的很好
tr1::shared_ptr支持定制型刪除器,使用它可以防范跨DLL構建和刪除的問題,可以用它來自動解除互斥鎖條款19:設計class猶如設計type
1. 合理的構建class的構造函數、析構函數和內存分配函數以及釋放函數
2. 不能把初始化和賦值搞混了
3. 如果你的類需要被用來以值傳遞,復制構造函數應該設計一個通過值傳遞的版本
4. 你應該給你的成員變量加約束條件,保證他們是合法值,所以你的成員函數必 須擔負起錯誤檢查工作
5. 如果你是派生類,那么你應該遵守基類的一些規(guī)范,如析構函數是否為 virtural
6. 你是否允許你的class有轉換函數,,是否允許隱式轉換。如果你只允許 explicit構造函數存在,就得寫出專門負責執(zhí)行轉換的函數
7. 想清楚你的類應該有哪些函數和成員
8. 哪些應該設計為私有
9. 哪個應該是你的friend,以及將他們嵌套與另一個是否合理
10. 對效率,異常安全性以及資源運用提供了哪些保證
11. 如果你定義的不是一個新type,而是定義整個type家族,那么你應該定義一 個類模板
12. 如果只是定義新的字類以便為已有的類添加機制,說不定單純定義一個或多 個模板更好
條款20:寧以pass-by-reference-to-const替換pass-by-value
盡量以pass-by-reference-to-const替換pass-by-value,因為前者通常比較高效,比如在含有類的傳遞時,避免了多次構造函數和多次析構函數的調用,大大的提高了效率
但是對于某些,比如內置類型,迭代器,函數調用等最好以值傳遞的形式條款21:必須返回對象時,別妄想返回其reference
絕對不能返回指針或者一個引用指向一個臨時變量,因為它存在棧中,一旦函數調用結束返回那么你得到的將是一個壞指針,也不能使用static變量來解決,你可以通過返回值來解決條款22:將成員變量聲明為private
1. 為了保證一致性
2. 可以細微的劃分訪問和控制以及約束
3. 內部更改后不影響使用
protected并不比public更具封裝性
條款23:寧以non-member、non-friend、替換member函數
我們可以用non-member、non-friend函數來替換某些成員函數,可以增加類的封裝性,包裹彈性和擴充性條款24:若所有參數皆需要類型轉換,請為此采用non-member函數
如果你需要為某個函數的所有參數(包括被this指針所指的那個隱喻參數)進行類型轉換,那么這個函數必須是個non-member條款25:考慮寫出一個不拋出異常的swap函數
在你沒有定義swap函數的情況下,編譯器會為你調用通用的swap函數,但是有的時候那并不是高效的,因為默認情況它在置換如指針的時候把整個內存都置換
我們采取一種解決辦法
1. 在類中提供一個 public swap成員函數,并且這個函數不能拋出異常2. 在類的命名空間中提供一個non-member swap函數,并令它調用類中的swap函數
3. 如果你正在編寫一個類而不是模板類,為你的class特化std::swap函數,并令它調用你的swap函數,請在類中聲明 using std::swap,讓其暴露,使得編譯器自行選擇更合適的版本Part5五、實現
條款26:盡可能延后變量定義式的出現時間
定義一個變量,那么你就得承受這個變量的構造和析構的成本時間,所以在定義一個變量的時候我們應該盡可能的延后定義時間,在使用前定義,這樣避免我們定義了卻沒有使用它,造成浪費條款27:盡量少做轉型動作
舊式轉型是C風格的轉型,C++中提供四種新式轉型:- const_cast 通常被用來將對象的常量性轉除。它也是唯一有此能力的轉型操作符
dynamic_cast 主要用來執(zhí)行“安全向下轉型” ,也就是用來決定對某對象是否歸屬繼承體系中的某個類型。它是唯一無法由舊式語法執(zhí)行的動作,也是唯一可能耗費重大運行成本的轉型動作
reinterpret_cast 意圖執(zhí)行低級轉型,實際動作(及結果)可能取決于編譯器,這也就表示它不可移植。例如將一個pointer to int轉型為一個int。這一類轉型在低級代碼以外很少見。
static_cast 用來強迫隱式轉換,例如將non-const對象轉換為const對象,或將int轉為double等等,它也可以用來執(zhí)行上述多種轉換的反向轉換,例如將void* 指針轉為 type 指針,將pointer-to-base 轉為 pointer-ro-derived 。但它無法將 const 轉為 non-const ——這個只有const_cast才能辦到
舊式轉型使用的時機是,當要調用一個explicit構造函數對一個對象傳遞給一個
函數時,其他盡量用新式轉型
請記住以下:
1. 如果可以的話,避免dynamic_cast轉型,如果實在需要,則可以試著用別的 無轉型方案代替
2. 如果轉型是必要的,那么應該把他隱藏于某個函數背后,客戶隨后可以調用該 函數,而不是需要將轉型放進自己的代碼里
3. 寧可要新型轉型,也不要使用舊式轉型
條款28:避免返回handles指向對象內部成分
避免返回handle(包括引用,指針和迭代器)指向對象內部。這樣可以增加封裝性,也能把出現空懸指針的可能性降低條款29:為“異常安全”而努力是值得的
異常安全函數提供以下三個保證之一:基本承諾:如果異常被拋出,程序內的任何事物仍然保持在有效狀態(tài)下。沒有任何對象或數據會因此而敗壞,所有對象都處于一種內部前后一致的狀態(tài)。然而程序的現實狀態(tài)恐怕不可預料強烈保證:如果異常被拋出,程序狀態(tài)不改變。調用這樣的函數需要有這樣的認知:如果函數成功,就是完全成功,如果函數失敗,程序會恢復到“調用之前”的狀態(tài)不拋擲保證:承諾絕不拋出異常,因為它們總是能夠完成他們原先承諾的功能。作用于內置類型身上所有操作都提供nothrow保證,這是異常安全碼中一個必不可少的關鍵基礎材料這三種保證是遞增的關系,但是如果我們實在做不到,那么可以提供第一個基本承諾,我們在寫的時候應該想如何讓它具備異常安全性 1. 首先以對象管理資源可以阻止資源泄漏
2. 在你能實現的情況下,盡量滿足以上的最高等級
條款30:透徹了解inlining 的里里外外
1. 隱喻的inline申請,即把定義寫在class內部
2. 明確聲明,即在定義式前加上關鍵字inline
將大多數inlining限制在小型、被頻繁調用的函數身上。這可使日后調試和二進制升級更容易,也可使得潛在的代碼膨脹問題最小化。不要只因為function templates出現在頭文件,就將他們聲明為inline條款31:將文件間的編譯依存關系降至最低
支持“編譯依存性最小化”的思想是:相依于聲明式,不要相依于定義式 1. 頭文件和實現相分離,頭文件完全且僅有聲明式
2. 使用創(chuàng)建接口類
Part6六、繼承與面向對象設計
條款32:確定你的public繼承塑模出is-a關系
public繼承意味著is-a的關系,即子類是父類的一種特殊化,適合基類的一定適合子類,每個派生類對象含有著父類對象的特點條款33:避免遮掩繼承而來的名稱
在父類中的名稱會被字類的名稱覆蓋,尤其是在public繼承下,沒有人希望這樣的發(fā)生
為了避免被遮掩,可以使用using聲明式或轉交函數,交給子類條款34:區(qū)分接口繼承和接口實現
聲明純虛函數的目的就是為了讓派生類只繼承函數接口
聲明虛函數的目的是讓派生類繼承該函數的接口和缺省實現
聲明普通函數的目的就是讓派生類強制接受自己的代碼,不希望重新定義條款35:考慮virtual函數以外的其他選擇
條款36:絕不重新定義繼承而來的non-virtual函數
任何情況下都不應該重新定義一個繼承而來的non-virtual函數條款37:絕不重新定義繼承而來的缺省參數值
絕對不要重新定義一個繼承而來的缺省參數值,因為缺省參數值都是靜態(tài)綁定的,而virtual函數——你唯一應該覆寫的東西是動態(tài)綁定條款38:通過復合塑模has-a或“根據某物實現出”
區(qū)分public繼承和復合
在應用領域,復合意味著一個中含有另一個,即has-a關系;在實現領域意味著根據某物實現出條款39:明智而審慎地使用private繼承
當需要復合時,盡可能的使用復合,必要時才使用private:
當protected成員或virtual函數牽扯進來的時候
當空間方面的利害關系,需要尺寸最小化條款40:明智而審慎地使用多重繼承
多重繼承時候,如果其父類又繼承同一個父類,所以解決的方式就是使用virtual繼承,即其父類同時以virtual繼承那個父類,但是相應的也會付出一些代價,例如時間更慢,需要重新定義父類的初始化等,因此設計時最好不要讓這個父類有任何數據成員當單一繼承和多重繼承都可以,那么最好選擇單一繼承,多重繼承也有正當的用途,可以實現同時public繼承和private繼承的組合Part7七、模板與泛型編程
條款41:了解隱式接口和編譯期多態(tài)
顯式接口:由函數的簽名式(也就是函數名稱、參數類型、返回類型)構成
隱式接口:不基于函數簽名式,而是由有效表達式組成
面向對象和泛型編程都支持接口和多態(tài),只不過一個以顯式為主,一個以隱式為主
兩種多態(tài)一個在運行期一個在編譯期條款42:了解typename的雙重意義
聲明模板參數的時候,class和typename是可以互換的,沒什么不一樣
但是標識嵌套從屬類型名稱的時候必須用typename
不得在基類列(繼承的時候)或成員初值列(初始化列表)內以它作為基類修飾符templete<typename T>
class Derived:public Base<T>::Nested{ //基類列表中不可以加“typename”
public:
explicit Derived(int x): Base<T>::Nested(x){//mem.init.list中不允許“typename”
typename Base<T>::Nested temp; //這個是嵌套從屬類型名稱
... //作為一個基類修飾符需要加上typename
}
}
條款43:學習處理模板化基類內的名稱
1. 在基類函數調用之前加上 this->
2. 使用 using 聲明式 ,告訴編譯器,請它假設這個函數存在
3. 指出這個函數在基類中,使用基類::函數的形式寫出來(不推薦這個,因為如 果是virtual函數,則 會影響動態(tài)綁定)
但是當有模板全特化的時候,確實使用的沒有這個函數,那么依然會報錯
條款44:將與參數無關的代碼抽離出來
模板生成多個類和多個函數,所以任何模板代碼都不該和某個造成膨脹的模板參數產生相依關系
因非類型模板參數造成的代碼膨脹,往往可以消除,做法是以函數參數或類成員變量替換模板參數
因類型模板參數造成的代碼膨脹,往往可以降低,做法是讓帶有完全相同的二進制表述 的具體類型共享實現碼條款45:運用成員函數模板接受所有兼容類型
使用成員函數模板可以生成接收所有兼容類型的函數
如果你聲明成員函數模板用來泛化拷貝構造函數和賦值操作,那么你還需要聲明正常的拷貝構造函數和賦值操作條款46:需要類型轉換時請為模板定義非成員函數
當我們編寫一個模板類,它提供的和這個模板祥光的函數支持所有參數的隱式類型轉換,請將哪些函數定義為模板類的內部的friend函數Part8八、定制new和delete
條款49:了解new—handler的行為
當new分配失敗的時候,它會先調用一個客戶指定的錯誤處理函數(set_new_handler),一個所謂的new—handler
它是一個typedef定義出一個指針指向函數,該函數沒有參數也不返回任何東西
set_new_handler的參數是個指針指向operator new 無法分配足夠內存時該被調用的函數。其返回值也是個指針,指向set_new_handler 被調用前正在執(zhí)行(馬上就要被替換)的那個new—handler函數
一個良好設計的new—handler函數必須做以下事情: 1. 讓更多內存可被使用。此策略的一個做法是,程序一開始就分配一大塊內存,而后當其第一次被調用,將它釋還給程序使用
2. 安裝另一個new—handler。可以設置讓其調用另一個new—handler來替換自己,用來做不同的事情,其做法是調用set_new_handler
3. 卸載new—handler,也就是將null指針傳給set_new_handler,這樣new在分配不成功時拋出異常
4. 拋出bad_alloc的異常。
5. 不返回,調用abort或exit
6. C++并部支持類的專屬new—handler,但其實也不需要。你可以令每個類提供自己的set_new_handler和operator new即可
set_new_handler允許客戶指定一個函數,在內存分配無法獲得滿足時調用。
Nothrow new是一個頗為局限的工具,因為它只適用于內存分配:后繼的構造函數調用還是可能拋出異常
條款50:了解new和delete的合理替換時機
替換operator new或operator delete的三個常見理由:用來檢測運用上的錯誤
為了收集使用上的統(tǒng)計數據
為了增加分配和歸還的速度
為了降低缺省內存管理器帶來的空間額外開銷,也就是實現內存池,可以節(jié)省空間為了彌補缺省分配器中的非最佳齊位
為了將相關對象成簇集中
為了獲得非傳統(tǒng)行為了解何時可在“全局性的”或“class專屬的”基礎上合理替換缺省的new和delete條款51:編寫new和delete時需固守常規(guī)
operator new應該內含有一個無窮的循環(huán),并在其中嘗試分配內存,如果它無法滿足內存需求,就該調用new-handler。它也應該有能力處理0 bytes申請,即將其按照1 byte分配。Class 專屬版本應該處理“比正確大小更大的(錯誤)申請”,因為當有字類繼承的時候,會出現傳入的大小和父類大小不同,所以要進行判斷形如if(size != sizeof(父類))operator delete應該在收到NULL指針的時候什么也不做,必要時交給全局的operator new來處理。條款52:寫了placement new也要寫placement delete
當你寫一個placement operator new ,請確定也寫了對應的placement operator delete版本。如果沒有這樣做,可能會發(fā)生隱微而時斷時續(xù)的內存泄露當你聲明placement new 和placement delete,請確定不要無意識(非故意)地遮掩正常的全局版本,你如果想提供自定義形式,請內含所有正常形式的new和delete或利用繼承機制及using聲明式Part9九、雜項討論
條款53:不要輕忽編譯器的警告
不同的編譯器有不同的警告標準,要嚴肅對待編譯器發(fā)出的警告信息。努力在你的編譯器的最高警告級別下爭取“無任何警告”的榮譽
不要過度依賴編譯器的報警能力,因為不同的編譯器對待事情的態(tài)度并不相同。一旦移植到另一個編譯器上,你原本依賴的警告信息有可能消失條款54:讓自己熟悉包括TR1在內的標準程序庫
C++標準程序庫的主要機能由STL、iostream、locales組成。并包含C99標準程序庫。TR1添加了智能指針(例如 tr1::shared_ptr)、一般化函數指針(tr1::function)、hash-based容器、正則表達式以及另外10個組件的支持
TR1自身知識一份規(guī)范。為了獲得TR1提供的好處,你需要一份實物。一個好的實物來源是Boost。條款55:讓自己熟悉Boost
Boost是一個社群,也是一個網站。致力于免費、源碼開放、同僚復審的C++程序庫開發(fā)。Boost在C++標準化過程中扮演具有影響力的角色
Boost提供許多TR1組件實現品,以及其他許多程序庫