GoogleCpp風格指南 3)類

3 類 Classes

類是C++中代碼的基本單元; 顯然, 它們被廣泛使用; 本節列舉了寫一個類時的主要注意事項;

3.1 構造函數的職責 Doing Work in Constructors

Tip 構造函數中只進行那些沒什麼意義的(trivial 譯註: 簡單初始化對於程序執行沒有實際的邏輯意義, 因爲成員變量"有意義"的值大多不再構造函數中確定)初始化, 可能的話, 使用 Init()方法集中初始化有意義的(non-trival)數據;

[Add] 在ctor中防止做複雜的初始化(特別是可能導致失敗的動作, 或者需要vitrual method的調用; <<<

定義: 在構造函數體中進行初始化操作;

優點: 排版方便, 無需擔心類是否已經初始化;

缺點: 

在構造函數中執行操作引起的問題有:

- 構造函數中很難上報錯誤, 不能使用異常; [可以使用函數範圍的異常機制 CLS::CLS try{} catch{}]

- 操作失敗會造成對象初始化失敗, 進入不確定狀態;

- 如果在構造函數內調用了自身的虛函數, 這類調用是不會重定向dispatched到子類的虛函數實現; 即使當前沒有子類化實現, 將來仍是隱患; [考慮構造的子類-基類相對順序, 基類構造時子類還未構造, 析構反過來也一樣]

- 如果有人創建該類型的全局變量(雖然違背了上節提到的規則), 構造函數將先 main()一步被調用, 有可能破壞構造函數中暗含的假設條件; 例如: gflags尚未初始化;

結論: 如果對象需要進行有意義的(non-trivial)初始化, 考慮使用明確的 Init()方法/factory函數並(或)增加一個成員標記用於指示對象是否已經初始化成功; 

[Add] 構造永遠不該調用virtual函數, 否則會產生一些非致命的錯誤non-fatal failures; <<<


3.2 默認構造函數

Tip 如果一個類定義了若干成員變量又沒有其他構造函數, 必須定義一個默認構造函數; 否則編譯器將自動產生一個糟糕的默認構造函數;

定義: new一個不帶icansh的類對象時, 會調用這個類的默認構造函數; 用 new[]創建數組時, 默認構造函數則總是被調用;

優點: 默認將結構體初始化爲"無效"值, 使調試更方便;

缺點: 對代碼編寫者來說, 這是多餘的工作;

結論: 

如果類中定義了成員變量, 而且沒有提供其他構造函數, 必須定義一個(不帶參數的)默認構造函數; 把對象的內部狀態初始化成一致/有效的值, 這無疑是更合理的方式;

這麼做的原因是: 如果沒有提供其他構造函數, 又沒有定義默認構造函數, 編譯器將自動生成一個; 編譯器生成的構造函數並不會對對象進行合理的初始化;

如果你定義的類繼承現有類, 而你又沒有增加新的成員變量, 則不需要爲新類定義默認構造函數;


[Add] 

初始化 Initialization

如果你的類定義了成員變量, 必須提供一個in-class的initializer, 爲每個成員變量進行初始化, 或者寫一個ctor(作爲默認構造); 如果你自己沒有聲明任何構造, 編譯器會爲你生成一個默認構造函數, 它會把很多域留在未初始化的狀態, 或者初始化成一個不合適的值;

定義: 

默認構造函數會在new一個沒有參數的class時被調用; 在調用 new[](數組)操作是總是會調用它; in-class的成員初始化意味着: 使用一個構造如 int count_ = 17; 或 string name_{"abc"}來聲明一個成員, 而不是僅僅 int count_; 或 string name_;

[c++11 http://stackoverflow.com/questions/13662441/c11-allows-in-class-initialization-of-non-static-and-non-const-members-what-c 類似Java的類內初始化]

優點: 

如果沒有提供intitializer, 一個用戶定義的默認ctor會被用來初始化對象; 它可以保證對象一旦被構造, 總是在合法valid和usable可用的狀態; 它還可以保證對象一開始創建的時候是一個明顯的"不可用impossible"的狀態, 有助於debugging;

缺點:

顯式地定義一個默認構造對於程序員來說是多餘的工作;

in-class成員初始化initialization可能會產生混淆, 如果一個成員變量在initialization中的部分內初始化, 然後又在構造函數中初始化, Note ctor中的值會覆蓋聲明時候的值, 這一點需要注意;

結論: 

使用 in-class成員initialization來做簡單的初始化, 特別當一個成員變量必須在多個ctor中做相同的初始化時;

如果你定義的成員變量沒有在in-class中被初始化, 而且也沒有其他的ctor, 你必須定義一個默認ctor(沒有參數的); 它可以更好地preferably將對象初始化, 保持它的內部狀態是一致的和合法的; 

之所以使用initialization是因爲如果你沒有其他的ctor, 也沒有定義一個默認ctor, 編譯器會自動生成一個; 這個編譯器生成的ctor爲你所做的初始化可能是不合理的(not sensibly);

如果你的類繼承自一個已存的類, 但是你添加了兩個新的成員變量, 你可能就不一定要寫一個默認構造了; [使用Initializer]

<<<


3.3 顯式構造函數 Explicit Constructors

Tip 對單個參數的構造函數使用C++關鍵字 explicit;

定義: 

通常, 如果構造函數只有一個參數, 可看成是一種隱式轉換conversion; 舉例, 如果定義了 Foo::Foo(string name), 接着把一個字符串傳給一個以Foo對象爲參數的函數, 構造函數 Foo::Foo(string name)將被調用, 並將該字符串轉換爲一個 Foo的臨時對象傳給調用函數; 這看似方便, 但如果你並不希望如此通過轉換生成一個新對象的話, 麻煩也隨之而來; 爲避免構造函數被調用造成隱式轉換, 可以將其聲明爲 explicit來防止這種隱式轉換;

優點: 避免不合時宜的變換;

缺點: 無;

結論: 

所有單參數構造函數都必須是顯式的; 在類定義中, 將關鍵字 explicit加到單參數構造函數前: explicit Foo(string name);

例外: 在極少數情況下, 拷貝構造函數可以不聲明成 explicit; 作爲其他類的透明包裝器的類也是特例之一; 類似的例外情況應在註釋中明確說明;

[Add] copy和move構造是特例: 他們不應該是explicit的, 爲了透明地transparent包裝另一些類的class也是特例; 這些特例應該被明顯地用comment解釋並標註

[c++11 && http://msdn.microsoft.com/zh-cn/library/dd293665.aspx ]

最好, 對於僅僅接受一個 std::initialize_list的ctor可以是non-explicit的; 這允許你類型是通過 braced initializer list構造的, 就像是一個assignment-style的初始化, 函數參數, 或返回語句; 例如:

1
2
3
MyType m = {1, 2};
MyType MakeMyType() { return {1, 2}; }
TakeMyType({1, 2});

[c++11 {} http://www.cplusplus.com/reference/initializer_list/initializer_list/ ]

<<<

[Add]

可拷貝和可移動的類型 Copyable and Movable Types

如果你的類型需要, 可以支持拷貝, 移動; 否則, 要禁止隱式地產生特殊的方法來拷貝和移動;

定義:

一個可拷貝的類型允許它的對象通過另一個同類型的對象被初始化或賦值, 無需改變源對象的值; 對於用戶定義的類型, 拷貝行爲是在拷貝構造和拷貝賦值操作符中被定義的; string是一個可拷貝的類型;

一個可移動的類型可以通過臨時值temporaries(所有可拷貝的類型是可移動的)來初始化和賦值; std::unique_ptr<int>是可移動但不可拷貝的類型; 對於用戶定義的類型, 移動希望是定義在 move ctor和 move-assignment操作符中的; 

[c++11 move && http://en.cppreference.com/w/cpp/language/move_constructor  http://en.cppreference.com/w/cpp/language/move_operator ]

copy/move ctor在某些情況下可以被編譯器隱式地調用, e.g. 按值傳遞對象時;

優點:

可拷貝和可移動的類型的對象可以在return時按值傳遞, 讓API保持簡單, 安全和更通用; 不像傳遞指針或引用, 沒有所有權ownership, 生命週期lifetime, 可修改性mutability和一些其他問題的困擾, 也不需要具體的約定; 它還可以防止客戶和實現之間非局部的non-local交互, 這樣更容易理解和維護; 這樣的對象可以用在通用的API裏面, 比如containers這樣需要按值傳遞的類型;

相比一些Clone(), CopyFrom(), Swap(), copy/move ctor/assignment operator通常更容易定義, 因爲它們可以由編譯器產生, 隱式地或者通過 = 默認操作; 它們是簡易的, 而且保證所有的數據成員被拷貝了; copy和move ctor通常也更高效, 它們不需要分配堆heap allocation或分開的初始化和賦值的步驟, 它們還適合作爲優化, 比如 copy elision; (http://en.cppreference.com/w/cpp/language/copy_elision  ) [複製省略 http://en.wikipedia.org/wiki/Copy_elision]

move操作允許對右值對象rvalue進行隱式而且高效地資源轉移; 它允許你在某些情況下使用更簡單plainer的編碼風格;

缺點:

許多類型並不需要是copyable的, 爲它們提供拷貝構造會令人困惑, 無意義nonsensical, 甚至徹底錯誤的; copy/assignment操作符對於基本類型來說是多餘的hazardous, 因爲使用它們會導致對象切割(http://en.wikipedia.org/wiki/Object_slicing) 默認的或無意地實現的拷貝操作符是錯誤的, 因此引起的bug可能是令人困惑和難以判斷的;

copy ctor會隱式地被調用, 容易被忽視; 這也會造成困惑, 特別對於程序員來說, 如果他熟悉的是約定或強制使用pass-by-reference的語言, 就容易發生這種情況; 它也會帶來過度拷貝Excessive copying, 造成性能問題;

結論: 

如果copyable/movable對於你的類型有用的話, 就創建它們, 在其他的API中使用是會起到作用; 基本規則, 如果copy行爲(包括計算複雜度computational complexity)對你的類型來說作用不明顯, 就不應該是copyable的; 如果你讓類型變成copyable, 要定義兩個copy operations(ctor和assignment); 如果你的類型是copyable而且一個move操作比copy更有效率, 就定義兩個move operations(ctor和assignment);

如果你的類型不是copyable的, 但是move對於這個類型的用戶來說是正確的操作, 而且這個類型的域支持, 可以爲類型定義兩個move operations, 將它變爲move-only的; 

建議默認用 = 來定義copy和move; 定義非默認的move操作需要一個風格特例style exception; 記住要評審一下默認操作符的正確性;

由於有slicing的風險, 如果僅僅是爲了繼承(防止從這樣的類中繼承這樣的成員), 不要爲類提供一個assignment operator或public的 copy/move ctor; 如果基類需要是copyable, 可以提供一個virtual Clone()方法, 一個protected copy ctor, 這樣可以讓子類來實現它;


委託和繼承構造 Delegating and Inheriting Constructors

使用委託和繼承ctor來減少代碼重複;

[c++11 delegating http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=296  inheriting http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=445  ]

定義:

委託和繼承ctor是兩個不同的特性, 都是從c++11引入的, 可以有效減少ctor中的代碼重複; 

委託ctor允許類的一個ctor把工作轉發給了一個ctor, 使用初始化列表語法中的特殊變量; e.g.

1
2
3
4
5
X::X(const string& name) : name_(name) {
  ...
}
 
X::X() : X("") { }

繼承ctor允許一個子類直接使用基類的ctor, 就像是基類的其他成員函數一樣, 但是必須要重新聲明基類ctor; 對於基類有多個構造的時候特別有用; e.g.

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
 public:
  Base();
  Base(int n);
  Base(const string& s);
  ...
};
 
class Derived : public Base {
 public:
  using Base::Base;  // Base's constructors are redeclared here.
};

優點: 

委託和繼承ctor減少了冗餘性verbosity和公式化boilerplate代碼, 有益於可讀性;

Java程序員對委託ctor很熟悉;

缺點:

委託ctor其實和使用一個helper方法是近似的approximate;

如果一個子類引入了新的成員變量, 由於基類構造並不知道它們的存在, 繼承ctor可能會造成混亂;

結論:

如果可以減少公式化代碼增加可讀性, 就使用委託和繼承ctor; 如果子類有新的成員變量時要小心使用繼承ctor; 如果你能爲子類成員變量使用in-class成員初始化, 繼承ctor仍然是適用的appropriate;

<<<


[Remove]

3.4 拷貝構造函數

Tip 僅在代碼中需要拷貝一個類對象的時候使用拷貝構造函數; 大部分情況下都不需要, 此時應使用 DISALLOW_COPY_AND_ASSIGN; [QT中也有: Q_DISABLE_COPY]

定義: 拷貝構造函數在複製一個對象到新建對象時被調用(特別是對象傳值時);

優點: 拷貝構造函數使得拷貝對象更加容易; Note STL容器要求所有內容可拷貝, 可賦值;

缺點: C++中的隱式對象拷貝是很多性能問題和bug的根源; 拷貝構造函數降低了代碼可讀性, 相比傳引用, 跟蹤傳值的對象更加困難, 對象修改的地方變得難以捉摸;

結論: 

大部分類並不需要可拷貝, 也不需要一個拷貝構造函數或重載賦值運算符; 不幸的是, 如果你不主動聲明它們, 編譯器會自動生成, 而且是public的;

Note 可以考慮在類的private中添加拷貝構造函數和賦值操作的空實現, 只有聲明沒有定義; 由於這些空函數聲明爲private, 當其他代碼試圖使用它們的時候, 編譯器將報錯; 方便起見, 可以使用 DISALLOW_COPY_AND_ASSIGN宏;

1
2
3
4
5
6
// 禁止使用拷貝構造函數和 operator= 賦值操作的宏
// 應該在類的 private: 中使用
 
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
            TypeName(const TypeName&); \
            void operator=(const TypeName&)

在 class Foo中:

1
2
3
4
5
6
7
8
class Foo {
    public:
        Foo(int f);
        ~Foo();
 
    private:
        DISALLOW_COPY_AND_ASSIGN(Foo);
};

如上所述, 絕大多數情況下都應該使用 DISALLOW_COPY_AND_ASSIGN宏; 如果類確實需要可拷貝, 應該在該類的頭文件中說明原由, 併合理的定義拷貝構造函數和賦值操作; 注意在 operator=中檢測自我賦值的情況; (譯註: 即 operator=接收的參數是該對象本身) [利用tag, 地址, operator==等方式判別];

爲了能作爲STL容器的值, 你可能有使類可拷貝的衝動, 在大多數類似的情況下, 真正該做的是把對象的指針放到STL容器中; 可以考慮使用 std::tr1::shared_ptr; [智能指針代替對象]

<<<


3.5 結構體 vs 類 Structs vs. Classes

Tip 對只有數據的被動對象passive object, 使用 struct, 其他一概使用 class;

在C++中, struct和 class關鍵字幾乎含義一樣, 我們爲這兩個關鍵字添加自己的語義理解, 以便爲定義的數據類型選擇合適的關鍵字;

struct用來定義包含數據的被動式對象, 也可以包含相關的常量, 但除了存取數據成員之外, 沒有別的函數功能; 並且存取功能是通過直接訪問位域(field), 而非函數調用; 除了構造函數, 析構函數, Initialize(), Reset(), Validate()外, 不能提供其他功能的函數;

如果需要更多的函數功能, class更適合, 如果拿不準, 就用class;

爲了和STL保持一致, 對於仿函數(functors)和特性(traits)可以不用class而是使用struct; [函數對象, 函數類型]

Note 類和結構體的成員變量使用不同的命名規則; [e.g. m_var和 _var]


3.6 繼承 Inheritance

Tip 使用組合(composition, 譯註: 這一點也是GOF在"Design Pattern"裏反覆強調的)常常比使用繼承更合理; 如果使用繼承的話, 定義爲 public繼承;

定義: 

當子類繼承基類時, 子類包含了父基類所哦有數據及操作的定義; C++實踐中, 繼承主要用於兩種場合: 實現繼承(implementation inheritance), 子類繼承父類的實現代碼; 接口繼承(interface inheritance), 子類僅繼承父類的方法名稱;

優點: 

實現繼承通過原封不動的複用基類代碼減少了代碼量; 由於繼承是在編譯時聲明, 程序員和編譯器都可以理解相應操作並發現錯誤; 從編程的角度而言, 接口繼承是用來強制類輸出特定的API; 在類沒有實現API中某個必須的方法時, 編譯器同樣會發現並報告錯誤;

缺點: 

對於實現繼承, 由於子類的實現代碼散佈在父類和子類之間, 要理解其實現變得更加困難; 子類不能重寫父類的非虛函數, 當然也就不能修改其實現; [如果函數簽名不同, 但名字相同, 就會出現重寫--基類函數被隱藏] 基類也可能定義了一些數據成員, 還要區分基類的實際physical佈局;

結論: 

所有繼承必須是public的; 如果你想使用私有繼承, 應該使用成把基類的實例作爲成員對象的方式; 

不要過度使用實現繼承; 組合常常更合適一些; 儘量做到只在"是一個"(is-a, 譯註: 其他 has-a情況下請使用組合)的情況下使用繼承; 如果 Bar的確"是一種" Foo, Bar才能繼承Foo;

必要的話, 析構函數聲明爲 virtual; 如果你的類有虛函數, 則析構函數也應該爲虛函數; 

Note 數據成員在任何情況下都必須是私有的; 儘量不要將成員函數寫成protected, 使得子類可以獲得訪問權;

當重載一個虛函數, 在衍生類中把它明確地聲明爲 virtual; 理論依據: 如果省略 virtual關鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數是否是虛函數; 

[Add] 用一個override或final(不常用的)specifier顯式地註釋annotate 一個vitrual函數或vitrual dtor; c++11之前使用virtual關鍵字只是作爲一個次級的註釋替代; 澄清一下, 聲明一個override時, 要明確使用override, final, 或virtual其中的一個; 原理Rationale: 一個函數或dtor標註爲override或final, 而不是對基類virtual函數的override, 會造成編譯失敗, 這樣可以幫助找到常見錯誤;  specifier作用就像文檔一樣: 如果沒有specifier, 讀者不得不去檢查所有的父類, 來判斷該函數是否爲virtual的;

[http://en.cppreference.com/w/cpp/language/final  http://en.cppreference.com/w/cpp/language/override  ]

<<<


3.7 多重繼承 Multiple Inheritance

Tip 真正需要用到多重實現繼承的情況非常少, 只在以下情況我們才允許多重繼承: 最多隻有一個基類是非抽象類, 其它基類都是以 Interface爲後綴的純接口類;

定義: 多重繼承允許子類擁有多個基類; 要作爲純接口的基類和具有實現的基類區別開來;

優點: 相比單繼承, 多重實現可以複用更多的代碼; 

缺點: 真正需要用到多重實現繼承的情況非常少, 多重實現繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案;

結論: 只有當所有父類除了第一個外都是純接口類時, 才允許使用多重繼承, 爲確保它們是純接口, 這些類必須以 Interface爲後綴; [有的寫法是前綴 I -- Command : public ICommand]

NOTE 關於該規則, Windows下有個特例;

[菱形多重繼承, 虛擬繼承]


3.8 接口 Interfaces

Tip 接口是指滿足特定條件的類, 這些類以 Interface爲後綴(不強制);

定義: 

當一個類滿足以下要求時, 稱之爲純接口:

- 只有純虛函數("= 0")和靜態函數(除了下文提到的析構函數);

- 沒有非靜態數據成員;

沒有定義任何構造函數, 如果有也不能帶有參數, 並且必須爲 protected; [構造也不能是純虛的]

- 如果它是一個子類, 也只能從滿足上述條件並以 Interface爲後綴的類繼承;

接口不能被直接實例化, 因爲它聲明瞭純虛函數; 爲確保接口的所有實現可被正確銷燬, 必須爲之聲明虛析構函數(作爲上述第1條規則的特例, 析構函數不能是純虛函數); 具體細節可參考 Stroustrup的 The C++ Programming Language, 3rd edition, 12.4; 

[可以純虛, 但是要有個實現 http://www.gotw.ca/gotw/031.htm 不過純虛的dtor完全沒有意義, 編譯器會自動生成默認的dtor ]

優點: 

以 Interface爲後綴可以提醒其他人不要爲該接口類增加函數實現或非靜態數據成員, 這一點對於 多重繼承尤其重要; 另外, 對於Java程序員來說, 接口的概念已經深入人心;

缺點: 

Interface後綴增加了類名長度, 爲閱讀和理解帶來了不便; 同時, 接口特性作爲實現細節不應暴露給用戶;

結論: 

只有在滿足上述需要時, 類才以 Interface結尾, 但反過來, 滿足上述需要的類未必一定以 Interface結尾;


3.9 運算符重載 Operator Overloading

Tip 除少數特定情景外, 不要重載運算符;

定義: 一個類可以定義諸如 + 和 / 等運算符, 使其可以像內建類型一樣直接操作; [Add]一個 operator""甚至允許內建的字面量語法去創建類對象;<<

優點: 

使代碼看上去更加直觀, 類表現的和內建類型(如int)行爲一致, 重載運算符使 Equals(), Add()等函數名黯然失色; 

爲了使一些模板函數正確工作, 你可能必須定義操作符;

[Add] 用戶自定義的字面量可以作爲明確的標記, 簡便地創建用戶自定義類型的對象;

缺點:

雖然操作符重載令代碼更加直觀, 但也有一些不足drawback:

- 混淆視聽, 讓你誤以爲一些耗時的操作和操作內建類型一樣輕巧;

- 更難定位重載運算符的調用點call site, 查找 Equals()顯然比對應的 == 調用點要容易得多;

- 有的運算符可以對指針進行操作, 容易導致bug; Foo + 4 做的是一件事, 而 &Foo +4 可能做的是完全不同的另一件事; 對於二者, 編譯器都不會報錯, 使其很難調試;

[Add] 

- 用戶自定義的字面量可以創建新的語法形式, 這樣對C++老手來說也會覺得不習慣<<< 

Note 重載還有令人喫驚的副作用ramification; 比如, 重載了 operator&的類不能安全地被前置聲明; 

[http://stackoverflow.com/questions/176559/you-cant-forward-declare-classes-that-overload-operator  前置聲明, 然後使用 operator&, 會造成未定義的情況]

結論:

一般不要重載運算符; 尤其是賦值操作(operator=)比較詭異, 應避免重載; 如果需要的話, 可以定義類似 Equal(), CopyFrom()等函數; 

[Add]同樣地, 如果一個類可能被前置聲明, 那麼要不惜代價地防止危險的unary operator&; 

不要重載 operator"", i不要引入用戶自定義的字面量; [c++11 http://en.cppreference.com/w/cpp/language/user_literal ]<<<

然而, 極少數情況下可能需要重載運算符以便與模板或"標準"C++類互操作interoperate (如 operator<<(ostream&, const T&)); 只有被證明是完全合理的才能重載, 但還是要儘可能避免這樣做; 尤其是不要僅僅爲了在STL容器中用作鍵值就重載 operator==或 operator<; 相反, 你應該在聲明容器的時候, 創建相等判斷和大小比較的仿函數類型;

有些STL算法確實需要重載 operator==時, 你可以這麼做, 記得別忘了在文檔中說明原因;

參考拷貝構造函數函數重載;


3.10 存取控制 Access Control

Tip 將所有數據成員聲明爲 private, 並根據需要提供相應的存取函數; (技術原因, 使用 Google Test時, 允許test fixture class的數據成員成爲protected的 [http://en.wikipedia.org/wiki/Test_fixture]  ) 

典型地, 某個名爲 foo_ 的變量, 其取值函數是 foo(); 還可能需要一個賦值函數 set_foo(); [Add] 特例: static const 數據成員(一般爲 kFoo)不必爲private的; <<<

在頭文件中定義的存取函數一般都是內聯的; 

參考繼承函數命名;


3.11 聲明順序 Declaration Order

Tip 在類中使用特定的聲明順序: public: 在 private: 之前, 成員函數在數據成員(變量)之前, etc.

類的訪問控制區段的聲明順序依次爲: public: protected: private:  如果某區段沒內容, 可以不聲明;

每個區段內的聲明通常按以下順序:

- typedefs 和枚舉;

- 常量(static const數據成員);

- 構造函數;

- 析構函數;

- 成員函數, 含靜態成員函數;

- 數據成員(除了static const數據成員)

[Add] 友元聲明應該總是在private中;<< 宏 DISALLOW_COPY_AND_ASSIGN的調用(disable copy&assign)放在 private區段的末尾, 它通常是類的最後部分, 參考拷貝構造函數;

.cc文件中函數的定義應儘可能和聲明順序一致;

不要在類定義中內聯大型函數; 通常, 只有那些無意義的trivial或性能要求高, 並且是比較短小的函數才能被定義爲內聯函數; 更多細節參考內聯函數;


3.12 編寫簡短的函數 Write Short Functions

Tip 傾向編寫簡短, 凝練的函數; 

我們承認長函數有時是合理的, 因此並不硬性限制函數的長度; 如果函數超過40行, 可以思索一下能不能在不影響程序結構的前提下對其進行分割;

即使一個長函數現在工作的非常好, 一旦有人對其修改, 有可能出現新的問題, 甚至導致難以發現的bug, 使函數儘量簡短, 便於他人閱讀和修改代碼;

在處理代碼時, 你可能會發現複雜的長函數; 不要害怕intimidated修改現有代碼, 如果證實這些代碼使用/調試困難, 或者你需要使用其中的一小段代碼, 考慮將其分割爲更加簡短並易於管理的若干函數;


譯者筆記

1) 不在構造函數中做太多邏輯相關的初始化;

2) 編譯器提供的默認構造函數不會對變量進行初始化, 如果定義了其他構造函數, 編譯器不再提供, 需要編碼者自行提供默認構造函數; [其實編譯器會初始化, 只是初始化的動作不保證正確]

3) 爲避免隱式轉換, 需將單參數構造函數聲明爲 explicit;

4) 爲避免拷貝構造函數, 賦值操作的濫用和編譯器自動生成, 可將其聲明爲 private且無需實現;

5) 僅在作爲數據集合時使用 struct;

6) 組合 > 實現繼承 > 接口繼承 > 私有繼承, 子類重載的虛函數也要聲明 virtual關鍵字, 雖然編譯器允許不這樣做;

7) 避免使用多重繼承, 使用時, 除一個基類含有實現外, 其他基類均爲純接口;

8) 接口類類名以 Interface爲後綴, 除提供實現的虛析構函數, 靜態成員函數外, 其他均爲純虛函數, 不定義非靜態數據成員, 不提供構造函數, 提供的話聲明爲protected;

9) 爲降低複雜性, 儘量不重載操作符, 模板, 標準類中使用時提供文檔說明;

10) 存取函數一般內聯在頭文件中;

11) 聲明次序 public - protected - private;

12) 函數體儘量短小, 緊湊, 功能單一; 

---TBC---YCR

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章