頭文件 1.1. Self-contained 頭文件
頭文件應該能夠自給自足(self-contained,也就是可以作為第一個頭文件被引入),以 .h 結尾。至于用來插入文本的文件,說到底它們并不是頭文件,所以應以 .inc 結尾。不允許分離出 -inl.h 頭文件的做法.1.2. #define 保護
所有頭文件都應該使用 #define 來防止頭文件被多重包含, 命名格式當是: <PROJECT>_<PATH>_<FILE>_H_ .1.3. 前置聲明
盡可能地避免使用前置聲明。使用 #include 包含需要的頭文件即可。結論:
盡量避免前置聲明那些定義在其他項目中的實體.函數:總是使用 #include.類模板:優先使用 #include.1.4. 內聯函數
只有當函數只有 10 行甚至更少時才將其定義為內聯函數. 另一個實用的經驗準則: 內聯那些包含循環或 switch 語句的函數常常是得不償失 (除非在大多數情況下, 這些循環或 switch 語句從不被執行). 有些函數即使聲明為內聯的也不一定會被編譯器內聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常內聯. 通常, 遞歸函數不應該聲明成內聯函數.1.5. #include 的路徑及順序
使用標準的頭文件包含順序可增強可讀性, 避免隱藏依賴:dir2/foo2.h (優先位置, 詳情如下)C 系統文件C++ 系統文件其他庫的 .h 文件本項目內 .h 文件盡量避免前置聲明那些定義在其他項目中的實體.函數:總是使用 #include.類模板:優先使用 #include. 2.1. 名字空間 鼓勵在 .cc 文件內使用匿名名字空間. 使用具名的名字空間時, 其名稱可基于項目名或相對路徑. 禁止使用 using 指示(using-directive)。禁止使用內聯命名空間(inline namespace)名字空間將全局作用域細分為獨立的, 具名的作用域, 可有效防止全局作用域的命名沖突.
2.1.1. 匿名名字空間
/ .h 文件namespace mynamespace {// 所有聲明都置于命名空間中// 注意不要使用縮進class MyClass { public: … void Foo();};} // namespace mynamespace// .cc 文件namespace mynamespace {// 函數定義都置于命名空間中void MyClass::Foo() { …}} // namespace mynamespace2.2. 嵌套類 不要將嵌套類定義成公有, 除非它們是接口的一部分, 比如, 嵌套類含有某些方法的一組選項
2.3. 非成員函數、靜態成員函數和全局函數
有時, 把函數的定義同類的實例脫鉤是有益的, 甚至是必要的. 這樣的函數可以被定義成靜態成員, 或是非成員函數. 非成員函數不應依賴于外部變量, 應盡量置于某個名字空間內. 相比單純為了封裝若干不共享任何靜態數據的靜態成員函數而創建類, 不如使用 2.1. 名字空間。
定義在同一編譯單元的函數, 被其他編譯單元直接調用可能會引入不必要的耦合和鏈接時依賴; 靜態成員函數對此尤其敏感. 可以考慮提取到新類中, 或者將函數置于獨立庫的名字空間內.
2.4. 局部變量
將函數變量盡可能置于最小作用域內, 并在變量聲明時進行初始化.
如果變量是一個對象, 每次進入作用域都要調用其構造函數, 每次退出作用域都要調用其析構函數.
在循環作用域外面聲明這類變量要高效的多:
Foo f; // 構造函數和析構函數只調用 1 次for (int i = 0; i < 1000000; ++i) { f.DoSomething(i);}2.5. 靜態和全局變量
禁止使用 class 類型的靜態或全局變量:它們會導致難以發現的 bug 和不確定的構造和析構函數調用順序。不過 constexpr 變量除外,畢竟它們又不涉及動態初始化或析構。
改善以上析構問題的辦法之一是用 quick_exit() 來代替 exit() 并中斷程序。它們的不同之處是前者不會執行任何析構,也不會執行 atexit() 所綁定的任何 handlers. 如果您想在執行 quick_exit() 來中斷時執行某 handler(比如刷新 log),您可以把它綁定到 _at_quick_exit(). 如果您想在 exit() 和 quick_exit() 都用上該 handler, 都綁定上去。
綜上所述,我們只允許 POD 類型的靜態變量,即完全禁用 vector (使用 C 數組替代) 和 string (使用 const char [])。
如果您確實需要一個 class 類型的靜態或全局變量,可以考慮在 main() 函數或 pthread_once() 內初始化一個指針且永不回收。注意只能用 raw 指針,別用智能指針,畢竟后者的析構函數涉及到上文指出的不定順序問題。
注意「using 指示(using-directive)」和「using 聲明(using-declaration)」的區別。 注意別在循環犯大量構造和析構的低級錯誤。
類3.1. 構造函數的職責 不要在構造函數中進行復雜的初始化 (尤其是那些有可能失敗或者需要調用虛函數的初始化).
在構造函數中執行操作引起的問題有:
構造函數很難上報錯誤的,關鍵是不能使用異常啊 操作失敗會造成對象初始化失敗,進入不確定狀態。 如果在構造函數中內調用了自身的虛函數,這類調用是不會重定向到子類的虛函數實現,也就是不要在構造函數中調用自身的虛函數啦
當然了,如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的init 方法或者工廠模式.
3.3. 顯式構造函數 對單個參數的構造函數使用 C++ 關鍵字 explicit. 通常, 如果構造函數只有一個參數, 可看成是一種隱式轉換
為避免構造函數被調用造成隱式轉換, 可以將其聲明為 explicit.
除單參數構造函數外, 這一規則也適用于除第一個參數以外的其他參數都具有默認參數的構造函數, 例如 Foo::Foo(string name, int id = 42).
最后, 只有 std::initializer_list 的構造函數可以是非 explicit, 以允許你的類型結構可以使用列表初始化的方式進行賦值. 例如:
```MyType m = {1, 2};MyType MakeMyType() { return {1, 2}; }TakeMyType({1, 2});```3.4. 可拷貝類型和可移動類型 如果你的類型需要, 就讓它們支持拷貝 / 移動. 否則, 就把隱式產生的拷貝和移動函數禁用
std::unique_ptr 就是一個可移動但不可復制的對象的例子
拷貝/ 移動構造函數在某些情況下會被編譯器隱式調用,例如通過傳值得方式傳遞對象。
優點呢:比他們的替代方案更加容易定義,簡潔,能保證所有數據成員都會被復制,還有更好的是,不需要堆的分配和單獨的初始化和賦值操作。一鍵式^_^
缺點:過度濫用拷貝,存在對象切割的風險,不要給任何有派生類的對象提過賦值操作或者拷貝、移動構造函數,當然也不要去繼承這樣的成員函數的類。如果你想copy類的屬性,請提供public virtual Clone() 和一個protected 的拷貝構造函數以實現派生類的實現。
如果你的類不需要拷貝/移動操作,請顯示的通過=delete 或其他手段禁用。
3.5. 委派和繼承構造函數 在能夠減少重復代碼的情況下,使用委派和 繼承構造函數。避免你干多余的活。
通過特殊的初始化列表的語法,委派構造函數允許類的一個構造函數調用其他類的構造函數。
x::x(const stting &name):name_(name){...}x::x():x(""){}繼承構造函數:允許派生類直接調用基類的構造函數,如繼承基類的其他成員函數,而不需要重新聲明。當基類擁有多個構造函數時,會特別的有用。
class Base{public: Base(); Base(int n); ...};class Derived:public Base{public: using Base::Base; // Base's constructors are redeclared here.}如果派生類的構造函數只是調用基類的構造函數,而沒有其他行為時,這一功能特別有用。
不好之處在于,如果你在派生類中引入了新的成員變量,而你的基類中卻沒有引入,那基類就不知道你添加了新的成員變量。
3.6. 結構體 VS. 類
3.7. 繼承
使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <> 里反復強調的) 常常比使用繼承更合理. 如果使用繼承的話, 定義為 public 繼承.
繼承主要用于兩種場合: 實現繼承(implementation inheritance ),子類繼承父類的實現代碼;接口繼承在(interface inheritance), 子類僅繼承父類的方法名稱。
優點:實現繼承通過原封不動的復用基類代碼減少代碼量。由于繼承是在編譯器時聲明。 缺點: 對于實現繼承,由于子類的實現代碼是分散在父類和子類之間,要理解其實實現變得更加困難。子類不能重寫父類的非虛函數,當然也就不能修改實現,基類也可能定義了一些數據成員,還要區分基類的實際布局。
結論:所有繼承必須public的,如果你想使用私有繼承,你應該替換成把基類的實例作為對象的方式。
不要過度使用實現繼承,組合常常更適合一些,盡量做到只在“是一個(is-a)(has-a)”情況下使用繼承。
必要的話,析構函數聲明為virtual。如果你的類有虛函數,則析構函數也應該為虛函數。注意在任何情況下,數據成員在任何情況下都必須是私有的。
當重載一個虛函數,在衍生類中把它明確的聲明為virtual,理論依據:如果省略virtual關鍵字,代碼閱讀者不得不檢查所有父類。
3.8. 多重繼承 真正需要用到多重實現繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個基類是非抽象類,其它基類都是以interface為后綴的純接口類。
多重繼承允許子類擁有多個基類. 要將作為 純接口 的基類和具有 實現 的基類區別開來.
優點: 相比單繼承,多重實現繼承可以復用更多的代碼
缺點: 真正需要用到多重,實現繼承的情況少之又少,多重實現繼承看上去是不錯的解決方案,但你通常也可以找到一個更明確,更清晰的不同解決方案。
結論: 只有當所有父類除第一個外都是純接口時,才允許使用多重繼承,為確保它們是純接口,這些類必須以interface為后綴。
3.9 接口
接口是指滿足特定條件的類,這些類似Interface為后綴(不強制)
定義: 當一個滿足一下要求時,稱之為純接口:
只有純虛函數(“=0”)和靜態函數(除了下文提到的析構函數) 沒有非靜態數據成員 沒有定義任何構造函數。如果有,也不能帶有參數,并且為protected 如果它是一個子類,也只能從滿足上述條件并以Interface為后綴的類繼承。
接口類不能被直接實例化,因為它聲明了純虛函數,為確保接口類的所有實現可被正確銷毀,必須為之聲明虛析構函數。
優點: 以interface為后綴可以提醒其他人不要為該接口類增加函數實現或非靜態數據成員。這一點對于多重繼承尤其重要。
缺點: interface后綴增加了類名長度,為了閱讀和理解帶來不便,同時,接口特性作為實現細節不應暴露給用戶。
結論: 只有在滿足上述需求時,類才以interface結尾,但反過來,滿足上述需求的類未必以interface結尾。
3.10. 運算符重載 重載有個不好的地方,比如重載了Operator& 的類不能被前置聲明
這里有極少數情況下還是可以使用的,operator<<(ostram&,const T&)
3.11. 存取控制
將 所有 數據成員聲明為 private, 并根據需要提供相應的存取函數.
一般在頭文件中把存取函數定義成內聯函數.
3.11. 聲明順序
聲明的順序通常為 public protected private
每個區段的內聲明順序通常如下: typedefs 和枚舉
常量 構造函數 析構函數 成員函數,含靜態函數 數據成員,含靜態成員
有元聲明,應該放在private區段,如果宏定義了DISALLOW_COPY_AND_ASSIGN 禁用了拷貝和賦值,應當將其置于private 區段的末尾
3.12. 編寫簡短函數
小結:
1.避免構造函數的隱式轉化,將構造函數聲明為explicit 2.避免使用多重繼承,除了一個基類實現外,其他類必須為純接口 3.接口類類名以interface為后綴的,除提供帶虛析構函數,和靜態成員函數,其他的都為純虛函數。如果想提過非靜態成員, 構造函數的話,加上protected
4.來自Google的奇技
4.1 所有權和智能指針 動態內存分配的對象最好有單一且固定的所有主(owner),且通過智能指針傳遞所有權(ownership)
智能指針可以看做是* 和 -> 的對象來看。 std::unique_ptr 是C++11 中新推出的一種智能指針, 用來表示動態分配出的對象(獨一無二)的所有權。當std::unique_ptr 離開作用域就會被銷毀。還挺智能的哈、 std::shared_ptr 同樣可以表示動態分配對象(獨一無二的所有權)還可以共享、復制給共同擁有者,當最后一個結束了,就銷毀了。last game over, 銷毀對象。
爽的地方: 傳遞對象的所有權,開銷比copy還小,好吧,看來copy也是不很給力啊 傳遞所有權比【借用】指針或者引用簡單,關鍵是省掉了兩個用戶一起協調生命周期的工作。
省事的完成了所有權的登記工作 對于const對象來說,智能指針簡單易用,也比深度復制高效,這也就是是智能指針的存在。
煩惱: 智能指針,你當然必須的使用智能指針
所有權的登記工作在運行時進行,開銷就不那么小了 某些時刻共享對象沒有被銷毀,看看是不是引用了(cycle reference) 這樣就不太好了
當然,再怎么智能還是不能完全代替原生的指針的。
有些時候為了提高服務性能,并且發現操作對象是不可變的,比如說:
std::shared_ptr<const Foo>這個時候用共享所有權,可以避免昂貴的拷貝代價,so,如果一定要用,總有這樣的需求的時候,推薦用std::shared_ptr
4.2 cpplint cpplint.py 檢查風格錯誤。 雖然說還不是那么的完美,不過也是很有用的工具,誤報的時候,在行尾// NOLINT 或者在上一行加 // NOLINTNEXTLINE
5.其他C++ 特性
5.1 引用參數 所有引用參數都需要加上const 在c中要想修改變量的值,必須傳遞地址int foo(int *pval)
,但是C++中還可以用引用,int foo(const int &pval)
為啥要這樣用呢? 引用參數時防止出現(*pval)++
,目的明確,不接受空NULL
指針
不好嘛,容易誤導,引用是值變量,但是卻有指針的語義。 參數列表中,記得加const void Foo(const string &in,sring *out)
google code 硬性要求,傳入參數前必須加const的引用或值參,輸出參數為指針。除非用于交換。
有時候使用const T*
指針比const T&
更加明智, 如: 你要傳入空指針NULL。 函數要把地址傳遞給輸入參數。
5。2 右值引用
只在定義移動構造函數與移動賦值操作使用右值引用,不要使用std::forward
什么是右值引用呢? void foo(string &&s)
右值引用是一種只能綁定到臨時對象的引用的一種,如上聲明了一個其參數是string 的右值函數。
優點: 用于移動構造函數,只移動但是不發生拷貝,提高性能。 v1
是一個vector <string>
,則auto v2(std::move(1)
就能利用指針,而不用復制數據到v2了,效率提高了很多。
所以這個時候要高效率的使用STL庫,如: std::unique_str
std::move
什么時候可能使用呢? 只在定義一個移動構造函數或者是移動賦值操作時使用右值引用,記住不要使用std::forward
功能函數,可以使用std::move
來表示一個對象的移動而不是復制一個對象。
5.3. 函數重載
若要用好函數重載,最好能讓讀者看一調用點(call site)
編寫一個參數類型為const string&
,用一個const char* 重載它
這里有什么好處? 重載函數參數,令代碼更加直觀,模板化代碼需要重載,同時為使用者帶來便利。
缺點:當函數只重載了函數的部分體,這樣會令人迷惑。
5.4 缺省參數 我們不允許使用缺省參數,還是盡量的使用函數重載。
5.5 變長數組和alloca() 不允許使用變長數組alloca();
最大的缺點是容易引起內存越界,這樣就不好玩了。bug
應當改用更安全的allocater(分配器),就像std::vector 和std::unique_ptr
bool Base::Equal(Base *other) =0;bool Derived::Equal(Base *other){ Derived* that = danamic_cast<Derived*>(other); if(that == NULL){ return false; }}基于類型的判斷樹是一個很強的暗示, 它說明你的代碼已經偏離正軌了. 不要像下面這樣:
if (typeid(*data) == typeid(D1)) { … } else if (typeid(*data) == typeid(D2)) { … } else if (typeid(*data) == typeid(D3)) { …
如果在類層級中加入新的子類,像這樣的代碼通常會崩潰,為啥,而且,因為某個子類改變了屬性,而引發的問題,這樣是很難排查的。
5.9 類型轉換 在C++ 中使用類型轉換,如static_cast<>() 不要使用int y=(int) x,或int y=int (x);
優點:相對于C來說,C有時候在做強制類型轉換,如(int) 3.4 而有的時候是在做類型轉換 如:(int)”hello”
static_cast 替代C風格進行轉換 const_cast 去掉const 限定符 reinterpret_cast 指針類型和整型或其它指針之間進行不安全的相互轉換。
5.10 流 只在記錄時使用流
流用來代替printf() 和scanf()
優點:用流在打印時不用關心對象的類型。流的構造和析構函數會自動打開和關閉對應的文件。
缺點:流使得pread()等功能很難執行。如果不使用printf風格的格式化字符串,%.*s 用流處理性能很低,流不支持字符串操作符重載重新排序。而這一點對于軟件國際化很有用。
cout<< this ; // 輸出地址 cout<< *this; // 輸出值 由于<<操作符會被重載,編譯器不會報錯,這樣也是反對使用操作符重載的原因。
5.11 前置自增和自減
通常使用前置自增, 不考慮返回值的話,前置自增(++i)通常要比后自增(i++)效率高,why,因為前置增不會像后自增(或自減)要對表達式的值i進行拷貝。
如果是簡單數值(非對象),兩種都無所謂。對迭代器和模板類型,使用前置自增(自減)。
5.12 const用法
class Foo{ int Bar(char c) const;};表示不能修改類成員變量的狀態
優點:大家更容易理解如何使用變量,編譯器也更好的檢測類型。
缺點:const 是入侵性的:如果你向一個函數傳入const變量,函數聲明中 也必須對應const參數。否則需要const_cast 類型轉換。
5.13 constexpr 用法
定義真正的常量,或實現常量初始化。 表示運行時,編譯時都不可以改變。
5.14 整型 小心整型類型轉換和整型提升 int 與unsigned int 運算時,前者被提升為unsigned int,而有可能 溢出。
5.15 64 位下的可移植性
代碼應該對64和32位系統友好,處理打印,比較,結構體字節對齊注意 大多數編譯器都允許調整結構體字節對齊,gcc中用attribute((packed)
創建64位常量時使用LL或ULL作為后綴 int64_t my_value = 0x123456789LL; uint64_t my_mask = 3ULL<<48;
5.16 預處理宏
當需要使用宏的時候,盡量使用內聯函數、枚舉、常量帶之
宏有全局作用域,可能引發異常行為。測試時非常頭疼。
宏的特性 在代碼庫底層 用# 字符串化,用##連接。
下面的規則,避免宏帶來的一些問題 不要在.h后面定義宏。 在馬上要使用時才進行#define,使用后立即#undef 不要只是對已經存在的宏使用#undef,選擇一個不會沖突的名稱
5.17 0 ,nullptr 和NULL 整數用0 ,實數用0.0,指針用nullptr或NULL,字符串用’/0’
在C++11項目中使用nullptr, sizeof(NULL)就和sizeof(0)不一樣
5.18 sizeof
盡可能用sizeof(varname)代替sizeof(type)
新聞熱點
疑難解答