類是 C++ 中代碼的基本單元. 顯然, 它們被廣泛使用. 本節列舉了在寫一個類時的主要注意事項.
## 3.1. 構造函數的職責
> Tip
> 構造函數中只進行那些沒什么意義的 (trivial, YuleFox 注: 簡單初始化對于程序執行沒有實際的邏輯意義, 因為成員變量 “有意義” 的值大多不在構造函數中確定) 初始化, 可能的話, 使用 `Init()` 方法集中初始化有意義的 (non-trivial) 數據.
定義:在構造函數體中進行初始化操作.優點:排版方便, 無需擔心類是否已經初始化.缺點:
在構造函數中執行操作引起的問題有:
>
> - 構造函數中很難上報錯誤, [不能使用異常](#).
> - 操作失敗會造成對象初始化失敗,進入不確定狀態.
> - 如果在構造函數內調用了自身的虛函數, 這類調用是不會重定向到子類的虛函數實現. 即使當前沒有子類化實現, 將來仍是隱患.
> - 如果有人創建該類型的全局變量 (雖然違背了上節提到的規則), 構造函數將先 `main()` 一步被調用, 有可能破壞構造函數中暗含的假設條件. 例如, [gflags](http://code.google.com/p/google-gflags/) [http://code.google.com/p/google-gflags/] 尚未初始化.
結論:如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 `Init()` 方法并 (或) 增加一個成員標記用于指示對象是否已經初始化成功.
## 3.2. 默認構造函數
> Tip
> 如果一個類定義了若干成員變量又沒有其它構造函數, 必須定義一個默認構造函數. 否則編譯器將自動生產一個很糟糕的默認構造函數.
定義:`new` 一個不帶參數的類對象時, 會調用這個類的默認構造函數. 用 `new[]` 創建數組時,默認構造函數則總是被調用.優點:默認將結構體初始化為 “無效” 值, 使調試更方便.缺點:對代碼編寫者來說, 這是多余的工作.結論:
如果類中定義了成員變量, 而且沒有提供其它構造函數, 你必須定義一個 (不帶參數的) 默認構造函數. 把對象的內部狀態初始化成一致/有效的值無疑是更合理的方式.
這么做的原因是: 如果你沒有提供其它構造函數, 又沒有定義默認構造函數, 編譯器將為你自動生成一個. 編譯器生成的構造函數并不會對對象進行合理的初始化.
如果你定義的類繼承現有類, 而你又沒有增加新的成員變量, 則不需要為新類定義默認構造函數.
## 3.3. 顯式構造函數
> Tip
> 對單個參數的構造函數使用 C++ 關鍵字 `explicit`.
定義:通常, 如果構造函數只有一個參數, 可看成是一種隱式轉換. 打個比方, 如果你定義了 `Foo::Foo(string name)`, 接著把一個字符串傳給一個以 `Foo` 對象為參數的函數, 構造函數 `Foo::Foo(string name)` 將被調用, 并將該字符串轉換為一個 `Foo` 的臨時對象傳給調用函數. 看上去很方便, 但如果你并不希望如此通過轉換生成一個新對象的話, 麻煩也隨之而來. 為避免構造函數被調用造成隱式轉換, 可以將其聲明為 `explicit`.優點:避免不合時宜的變換.缺點:無結論:
所有單參數構造函數都必須是顯式的. 在類定義中, 將關鍵字 `explicit` 加到單參數構造函數前: `explicit Foo(string name);`
例外: 在極少數情況下, 拷貝構造函數可以不聲明成 `explicit`. 作為其它類的透明包裝器的類也是特例之一. 類似的例外情況應在注釋中明確說明.
## 3.4. 拷貝構造函數
> Tip
> 僅在代碼中需要拷貝一個類對象的時候使用拷貝構造函數; 大部分情況下都不需要, 此時應使用 `DISALLOW_COPY_AND_ASSIGN`.
定義:拷貝構造函數在復制一個對象到新建對象時被調用 (特別是對象傳值時).優點:拷貝構造函數使得拷貝對象更加容易. STL 容器要求所有內容可拷貝, 可賦值.缺點:C++ 中的隱式對象拷貝是很多性能問題和 bug 的根源. 拷貝構造函數降低了代碼可讀性, 相比傳引用, 跟蹤傳值的對象更加困難, 對象修改的地方變得難以捉摸.結論:
大部分類并不需要可拷貝, 也不需要一個拷貝構造函數或重載賦值運算符. 不幸的是, 如果你不主動聲明它們, 編譯器會為你自動生成, 而且是 `public` 的.
可以考慮在類的 `private:` 中添加拷貝構造函數和賦值操作的空實現, 只有聲明, 沒有定義. 由于這些空函數聲明為 `private`, 當其他代碼試圖使用它們的時候, 編譯器將報錯. 方便起見, 我們可以使用 `DISALLOW_COPY_AND_ASSIGN` 宏:
// 禁止使用拷貝構造函數和 operator= 賦值操作的宏
// 應該類的 private: 中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
在 `class foo:` 中:
class Foo {
public:
Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
如上所述, 絕大多數情況下都應使用 `DISALLOW_COPY_AND_ASSIGN` 宏. 如果類確實需要可拷貝, 應在該類的頭文件中說明原由, 并合理的定義拷貝構造函數和賦值操作. 注意在 `operator=` 中檢測自我賦值的情況 (yospaly 注: 即 `operator=` 接收的參數是該對象本身).
為了能作為 STL 容器的值, 你可能有使類可拷貝的沖動. 在大多數類似的情況下, 真正該做的是把對象的 _指針_ 放到 STL 容器中. 可以考慮使用 `std::tr1::shared_ptr`.
## 3.5. 結構體 VS. 類
> Tip
> 僅當只有數據時使用 `struct`, 其它一概使用 `class`.
在 C++ 中 `struct` 和 `class` 關鍵字幾乎含義一樣. 我們為這兩個關鍵字添加我們自己的語義理解, 以便為定義的數據類型選擇合適的關鍵字.
`struct` 用來定義包含數據的被動式對象, 也可以包含相關的常量, 但除了存取數據成員之外, 沒有別的函數功能. 并且存取功能是通過直接訪問位域 (field), 而非函數調用. 除了構造函數, 析構函數, `Initialize()`, `Reset()`, `Validate()` 外, 不能提供其它功能的函數.
如果需要更多的函數功能, `class` 更適合. 如果拿不準, 就用 `class`.
為了和 STL 保持一致, 對于仿函數 (functors) 和特性 (traits) 可以不用 `class` 而是使用 `struct`.
注意: 類和結構體的成員變量使用 [不同的命名規則](#).
## 3.6. 繼承
> Tip
> 使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <<Design Patterns>> 里反復強調的) 常常比使用繼承更合理. 如果使用繼承的話, 定義為 `public` 繼承.
定義:當子類繼承基類時, 子類包含了父基類所有數據及操作的定義. C++ 實踐中, 繼承主要用于兩種場合: 實現繼承 (implementation inheritance), 子類繼承父類的實現代碼; 接口繼承 (interface inheritance), 子類僅繼承父類的方法名稱.優點:實現繼承通過原封不動的復用基類代碼減少了代碼量. 由于繼承是在編譯時聲明, 程序員和編譯器都可以理解相應操作并發現錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現 API 中某個必須的方法時, 編譯器同樣會發現并報告錯誤.缺點:對于實現繼承, 由于子類的實現代碼散布在父類和子類間之間, 要理解其實現變得更加困難. 子類不能重寫父類的非虛函數, 當然也就不能修改其實現. 基類也可能定義了一些數據成員, 還要區分基類的實際布局.結論:
所有繼承必須是 `public` 的. 如果你想使用私有繼承, 你應該替換成把基類的實例作為成員對象的方式.
不要過度使用實現繼承. 組合常常更合適一些. 盡量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果 `Bar` 的確 “是一種” Foo, `Bar` 才能繼承 `Foo`.
必要的話, 析構函數聲明為 `virtual`. 如果你的類有虛函數, 則析構函數也應該為虛函數. 注意 [數據成員在任何情況下都必須是私有的](#).
當重載一個虛函數, 在衍生類中把它明確的聲明為 `virtual`. 理論依據: 如果省略 `virtual` 關鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數是否是虛函數.
## 3.7. 多重繼承
> Tip
> 真正需要用到多重實現繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個基類是非抽象類; 其它基類都是以 `Interface` 為后綴的純接口類.
定義:多重繼承允許子類擁有多個基類. 要將作為 _純接口_ 的基類和具有 _實現_ 的基類區別開來.優點:相比單繼承 (見 [繼承](#)), 多重實現繼承可以復用更多的代碼.缺點:真正需要用到多重 _實現_ 繼承的情況少之又少. 多重實現繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案.結論:只有當所有父類除第一個外都是 [純接口類](#) 時, 才允許使用多重繼承. 為確保它們是純接口, 這些類必須以 `Interface` 為后綴.
> Note
> 關于該規則, Windows 下有個 [特例](#).
## 3.8. 接口
> Tip
> 接口是指滿足特定條件的類, 這些類以 `Interface` 為后綴 (不強制).
定義:
當一個類滿足以下要求時, 稱之為純接口:
>
> - 只有純虛函數 (“`=0`”) 和靜態函數 (除了下文提到的析構函數).
> - 沒有非靜態數據成員.
> - 沒有定義任何構造函數. 如果有, 也不能帶有參數, 并且必須為 `protected`.
> - 如果它是一個子類, 也只能從滿足上述條件并以 `Interface` 為后綴的類繼承.
接口類不能被直接實例化, 因為它聲明了純虛函數. 為確保接口類的所有實現可被正確銷毀, 必須為之聲明虛析構函數 (作為上述第 1 條規則的特例, 析構函數不能是純虛函數). 具體細節可參考 Stroustrup 的 _The C++ Programming Language, 3rd edition_ 第 12.4 節.
優點:以 `Interface` 為后綴可以提醒其他人不要為該接口類增加函數實現或非靜態數據成員. 這一點對于 [多重繼承](#) 尤其重要. 另外, 對于 Java 程序員來說, 接口的概念已是深入人心.缺點:`Interface` 后綴增加了類名長度, 為閱讀和理解帶來不便. 同時,接口特性作為實現細節不應暴露給用戶.結論:只有在滿足上述需要時, 類才以 `Interface` 結尾, 但反過來, 滿足上述需要的類未必一定以 `Interface` 結尾.
## 3.9. 運算符重載
> Tip
> 除少數特定環境外,不要重載運算符.
定義:一個類可以定義諸如 `+` 和 `/` 等運算符, 使其可以像內建類型一樣直接操作.優點:使代碼看上去更加直觀, 類表現的和內建類型 (如 `int`) 行為一致. 重載運算符使 `Equals()`, `Add()` 等函數名黯然失色. 為了使一些模板函數正確工作, 你可能必須定義操作符.缺點:
雖然操作符重載令代碼更加直觀, 但也有一些不足:
- 混淆視聽, 讓你誤以為一些耗時的操作和操作內建類型一樣輕巧.
- 更難定位重載運算符的調用點, 查找 `Equals()` 顯然比對應的 `==` 調用點要容易的多.
- 有的運算符可以對指針進行操作, 容易導致 bug. `Foo + 4` 做的是一件事, 而 `&Foo + 4` 可能做的是完全不同的另一件事. 對于二者, 編譯器都不會報錯, 使其很難調試;
重載還有令你吃驚的副作用. 比如, 重載了 `operator&` 的類不能被前置聲明.
結論:
一般不要重載運算符. 尤其是賦值操作 (`operator=`) 比較詭異, 應避免重載. 如果需要的話, 可以定義類似 `Equals()`, `CopyFrom()` 等函數.
然而, 極少數情況下可能需要重載運算符以便與模板或 “標準” C++ 類互操作 (如 `operator<<(ostream&, const T&)`). 只有被證明是完全合理的才能重載, 但你還是要盡可能避免這樣做. 尤其是不要僅僅為了在 STL 容器中用作鍵值就重載 `operator==` 或 `operator<`; 相反, 你應該在聲明容器的時候, 創建相等判斷和大小比較的仿函數類型.
有些 STL 算法確實需要重載 `operator==` 時, 你可以這么做, 記得別忘了在文檔中說明原因.
參考 [拷貝構造函數](#) 和 [函數重載](#).
## 3.10. 存取控制
> Tip
> 將 _所有_ 數據成員聲明為 `private`, 并根據需要提供相應的存取函數. 例如, 某個名為 `foo_` 的變量, 其取值函數是 `foo()`. 還可能需要一個賦值函數 `set_foo()`.
一般在頭文件中把存取函數定義成內聯函數.
參考 [繼承](#) 和 [函數命名](#)
## 3.11. 聲明順序
> Tip
> 在類中使用特定的聲明順序: `public:` 在 `private:` 之前, 成員函數在數據成員 (變量) 前;
類的訪問控制區段的聲明順序依次為: `public:`, `protected:`, `private:`. 如果某區段沒內容, 可以不聲明.
每個區段內的聲明通常按以下順序:
>
> - `typedefs` 和枚舉
> - 常量
> - 構造函數
> - 析構函數
> - 成員函數, 含靜態成員函數
> - 數據成員, 含靜態數據成員
宏 `DISALLOW_COPY_AND_ASSIGN` 的調用放在 `private:` 區段的末尾. 它通常是類的最后部分. 參考 [拷貝構造函數](#).
`.cc` 文件中函數的定義應盡可能和聲明順序一致.
不要在類定義中內聯大型函數. 通常, 只有那些沒有特別意義或性能要求高, 并且是比較短小的函數才能被定義為內聯函數. 更多細節參考 [內聯函數](#).
## 3.12. 編寫簡短函數
> Tip
> 傾向編寫簡短, 凝練的函數.
我們承認長函數有時是合理的, 因此并不硬性限制函數的長度. 如果函數超過 40 行, 可以思索一下能不能在不影響程序結構的前提下對其進行分割.
即使一個長函數現在工作的非常好, 一旦有人對其修改, 有可能出現新的問題. 甚至導致難以發現的 bug. 使函數盡量簡短, 便于他人閱讀和修改代碼.
在處理代碼時, 你可能會發現復雜的長函數. 不要害怕修改現有代碼: 如果證實這些代碼使用 / 調試困難, 或者你需要使用其中的一小段代碼, 考慮將其分割為更加簡短并易于管理的若干函數.
## 譯者 (YuleFox) 筆記
1. 不在構造函數中做太多邏輯相關的初始化;
1. 編譯器提供的默認構造函數不會對變量進行初始化, 如果定義了其他構造函數, 編譯器不再提供, 需要編碼者自行提供默認構造函數;
1. 為避免隱式轉換, 需將單參數構造函數聲明為 `explicit`;
1. 為避免拷貝構造函數, 賦值操作的濫用和編譯器自動生成, 可將其聲明為 `private` 且無需實現;
1. 僅在作為數據集合時使用 `struct`;
1. 組合 > 實現繼承 > 接口繼承 > 私有繼承, 子類重載的虛函數也要聲明 `virtual` 關鍵字, 雖然編譯器允許不這樣做;
1. 避免使用多重繼承, 使用時, 除一個基類含有實現外, 其他基類均為純接口;
1. 接口類類名以 `Interface` 為后綴, 除提供帶實現的虛析構函數, 靜態成員函數外, 其他均為純虛函數, 不定義非靜態數據成員, 不提供構造函數, 提供的話,聲明為 `protected`;
1. 為降低復雜性, 盡量不重載操作符, 模板, 標準類中使用時提供文檔說明;
1. 存取函數一般內聯在頭文件中;
1. 聲明次序: `public` -> `protected` -> `private`;
1. 函數體盡量短小, 緊湊, 功能單一;
- Google 開源項目風格指南 (中文版)
- C++ 風格指南
- 0. 扉頁
- 1. 頭文件
- 2. 作用域
- 3. 類
- 4. 來自 Google 的奇技
- 5. 其他 C++ 特性
- 6. 命名約定
- 7. 注釋
- 8. 格式
- 9. 規則特例
- 10. 結束語
- Objective-C 風格指南
- Google Objective-C Style Guide 中文版
- 留白和格式
- 命名
- 注釋
- Cocoa 和 Objective-C 特性
- Cocoa 模式
- Python 風格指南
- Google Python 風格指南 - 中文版
- 背景
- Python語言規范
- Python風格規范
- 臨別贈言
- JSON 風格指南
- 簡介
- 定義
- 一般準則
- 屬性名準則
- 屬性值準則
- 屬性值數據類型
- JSON結構和保留屬性名
- 頂級保留屬性名稱
- data對象的保留屬性名
- 用于分頁的保留屬性名
- 用于鏈接的保留屬性名
- 錯誤對象中的保留屬性名
- 屬性順序
- 示例
- 附錄