1. 程式人生 > 實用技巧 >C++程式碼整潔之道

C++程式碼整潔之道

C++程式碼整潔之道

原文地址

整潔的程式碼在團隊中無疑是很受歡迎的,可以高效的被其它成員理解和維護,本文參考《C++程式碼整潔之道》和《Google C++編碼規範》,結合自己的一些想法整理如下:

C++本身作為面嚮物件語言,首先介紹下面向物件一般涉及到的開發原則。

面向物件開發原則

  1. 依賴倒置原則:針對介面程式設計,依賴於抽象而不依賴於具體,抽象(穩定)不應依賴於實現細節(變化),實現細節應該依賴於抽象,因為穩定態如果依賴於變化態則會變成不穩定態。
  2. 開放封閉原則:對擴充套件開放,對修改關閉,業務需求是不斷變化的,當程式需要擴充套件的時候,不要去修改原來的程式碼,而要靈活使用抽象和繼承,增加程式的擴充套件性,使易於維護和升級,類、模組、函式等都是可以擴充套件的,但是不可修改。
  3. 單一職責原則:一個類只做一件事,一個類應該僅有一個引起它變化的原因,並且變化的方向隱含著類的責任。
  4. 里氏替換原則:子類必須能夠替換父類,任何引用基類的地方必須能透明的使用其子類的物件,開放關閉原則的具體實現手段之一。
  5. 介面隔離原則:介面最小化且完備,儘量少public來減少對外互動,只把外部需要的方法暴露出來。
  6. 最少知道原則:一個實體應該儘可能少的與其他實體發生相互作用。
  7. 將變化的點進行封裝,做好分界,保持一側變化,一側穩定,呼叫側永遠穩定,被呼叫側內部可以變化。
  8. 優先使用組合而非繼承,繼承為白箱操作,而組合為黑箱,繼承某種程度上破壞了封裝性,而且父類與子類之間耦合度比較高。
  9. 針對介面程式設計,而非針對實現程式設計,強調介面標準化

C++開發原則

通過上述面向物件開發原則的理解可以細化到具體C++開發原則。

  • 保持簡單和直接原則(KISS, Keep it simple and stupid):保持程式碼儘可能簡單,如果需求需要的話,才在程式碼中引入靈活的可變點,只新增那些可使整體變得更簡單的區域性複雜的東西。
  • 不需要原則(YAGNI, You're not gonna need it):總是在你真正需要的時候再實現他們,而不是在你只是預見到你將來會需要他們而去實現,在真正需要的時候再寫程式碼,那時再重構也來得及。
  • 避免複製原則(DRY, Do not repeat yourself):不要複製,不要重複,這是相當危險的操作,你修改一處程式碼的時候總能記得去修改另外一處或另外多處你曾經複製的程式碼嗎?
  • 資訊隱藏原則:一段程式碼呼叫了另外一段程式碼,呼叫者不應該知道被呼叫者程式碼的實現,否則呼叫者就有可能修改被呼叫者的實現來實現某些功能,而這有可能引發其它呼叫者的bug。
  • 高內聚低耦合原則:類似單一職責原則,明確每個模組的具體責任,儘量少的依賴於其它模組。
  • 最少驚訝原則:函式功能要與函式名字功能一致,難道你要在一個getter()函式去更改成員變數的值嗎?
  • 更乾淨原則(自命名):離開露營地的時候,應讓露營地比你來之前還要乾淨,當發現程式碼中有需要改進或者風格不好的地方,應該立刻改掉,不要care這段程式碼的原作者是誰,也不要care這是誰的模組,程式碼所有權是集體的,每個團隊成員在任何時候都應該可以對任何程式碼進行更改和擴充套件。

關於面向物件設計原則可以參考一文讓你搞懂設計模式

注重單元測試

重要性就不多說了,防患於未然,構建大型系統尤其需要進行單元測試,保證程式碼質量,可以防患於未然。一般都講究測試驅動開發,開發一個功能首先要想好怎麼測試,先把測試程式碼寫好,再去開發對應的需求。通過單元測試也有利於開發者更好的進行介面的設計,主要說下良好的單元測試的原則。

單元測試的原則

  • 保證單元測試的程式碼的質量,單元測試的程式碼也是程式碼,不應該和產品程式碼區別對待,而且單元測試的程式碼再寫出bug更影響測試效率。
  • 單元測試的命名, 每個測試單元需要根據具體測試內容進行相應的命名,方便定位分析問題,好的命名如果出現問題時通過測試單元的名字基本就可以定位問題。
  • 保證單元測試的獨立性,每個測試單元都是獨立的,不依賴於其它測試單元,不要構建測試單元的上下文,上面的測試單元出問題影響到下面的單元測試的設計是很不友好的。
  • 儘量保證一個測試單元使用一個斷言,保證測試單元內部的一個相對獨立性,上面的斷言阻礙了下面的斷言測試也是不好的設計。
  • 保證單元測試環境的獨立,保證每個測試單元都有獨立的環境,不依賴於其它環境,每個測試單元都要是個獨立的可執行的例項,每個單元測試結束後記得清理環境。
  • 沒必要對第三方庫和外部系統做單元測試,只對自己寫的程式碼進行測試。
  • 單元測試儘量不要涉及資料庫,資料庫的狀態是全域性的,測試不能保證獨立性,而且資料庫的訪問也是緩慢的,影響單元測試的速度,如果真的需要可以模擬資料庫在內容中進行測試,其實通常是在系統整合和系統測試級別時去測試資料庫。
  • 不要混淆測試程式碼和產品程式碼,產品程式碼中不應依賴測試程式碼。
  • 測試必須要快速執行,確保秒級別,大型系統的單元測試也就幾分鐘而已,單元測試不要訪問資料庫、磁碟、網路等外設。
  • 找一些測試替身,例如有些資料需要通過網路獲取,那可以利用依賴注入做一個網路替身的類模擬這些資料的產生,可以研究研究Google mock。

良好的命名

無論是什麼語言,函式和變數的良好命名都是很有必要的,通過函式的名字我們就可以知道這個函式裡程式碼的作用,而不是通過寫註釋,個人一直傾向於用程式碼自解釋。

檔案命名

檔名字要全部小寫,中間用_相連,字尾名為.cc和.h

型別命名

型別名稱的每個單詞首字母均大寫, 不包含下劃線: MyExcitingClass, MyExcitingEnum.

變數命名

不要將變數的型別在名字中體現,這樣以後變數型別改變的話還需要去改動變數名,充分利用IDE的功能,變數 (包括函式引數) 和資料成員名一律小寫, 單詞之間用下劃線連線. 類的成員變數以下劃線結尾, 但結構體的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.

class TableInfo {
    ...
    private:string table_name_; // 好 - 後加下劃線.
    string tablename_;   // 好.
    static Pool<TableInfo>* pool_; // 好.
    int i_table; // 不好,不要將變數的型別在名字中體現
};

常量命名

宣告為 constexpr 或 const 的變數, 或在程式執行期間其值始終保持不變的, 命名時以 “k” 開頭, 大小寫混合

const int kDaysInAWeek = 7;

函式命名

常規函式使用大小寫混合, 取值和設值函式則要求與變數名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().

列舉命名

和常量一致

enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};

Tip:

除非像swap函式裡tmp那種一目瞭然,否則不要搞無意義的命名,函式名變數名字寧可特別長也要寫清楚究竟是什麼意思,不要用縮寫,一個變數儘量在臨近使用前才定義,可讀性強也可更好利用cpu cache。

編輯器

團隊可以統一使用相同的編輯器,個人目前使用的是VS Code編輯器,同時每個專案使用統一的.clang_format檔案,統一規範程式碼格式,所有的換行符都要用LF格式,不要用CRLF格式,在右下角可以設定

個人的.clang-format檔案如下,是在google風格的基礎上做了些修改:

BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 120
SortIncludes: true
MaxEmptyLinesToKeep: 2

C++編碼規範要點小總結

  • 每個標頭檔案都要使用#define避免被重複引用
命名格式 <PROJECT>_<PATH>_<FILE>_H_
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

或使用#pragma once,而#define方式更通用

  • 鼓勵在 .cc 檔案內使用匿名名稱空間或 static 宣告. 使用具名的名稱空間時, 其名稱可基於專案名或相對路徑. 禁止使用 using 指示, 禁止使用內聯名稱空間(inline namespace)
  • 一行儘量不要超過120個字元,一個函式儘量不要超過40行,同時一個檔案儘量控制在500行內.
  • 所有的引用形參如不做改動一律加const,在任何可能的情況下都要使用 const或constexpr
  • new記憶體的地方儘量使用智慧指標,c++11 就儘量用std::unique_ptr替代std::auto_ptr
  • 合理使用移動語義,減少記憶體拷貝,參考左值引用、右值引用、移動語義、完美轉發,你知道的不知道的都在這裡
  • 禁止使用 RTTI,儘量在編譯期間就確定引數型別,不要搞執行時識別typeid這種程式碼
  • 使用 C++ 的型別轉換, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等轉換方式
  • 明確使用前置++還是後置++的具體含義,如不考慮返回值,儘量使用效率高的前置++ (++i)
  • 不要使用uint型別,如果需要使用大整型可以考慮int64,否則型別的隱式型別轉換會帶來很多麻煩
  • 如無特殊必要不要使用巨集,可以考慮使用const或constexpr替代巨集,巨集的全域性作用域很麻煩,如果非要用在馬上要使用時才進行 #define, 使用後要立即 #undef
  • google文件說一定不要用巨集來控制條件編譯(但是我自己還沒有查到不用巨集如何控制條件編譯,或許就不要搞條件編譯)
  • 儘可能用 sizeof(varname) 代替 sizeof(type).使用 sizeof(varname) 是因為當代碼中變數型別改變時會自動更新. 您或許會用 sizeof(type) 處理不涉及任何變數的程式碼,比如處理來自外部或內部的資料格式,這時用變數就不合適了
  • 型別名如果過長的話可以考慮使用auto關鍵字
  • 註釋統一使用 // ,不要通過註釋禁用程式碼,擅用git,不要為易懂的程式碼寫註釋
  • 寫完程式碼後記得format,VS Code(windows快捷鍵) shift + alt + F ,每個專案最好都有統一的.clang_format檔案
  • 使用C++的string和stream替代C語言風格的char*,使用std::ostream和std::cout替代printf()、sprintf()等
  • 儘量使用STL標準庫的容器而不是C語言風格的陣列,陣列的越界訪問之類當時是不會報錯的,反而可能弄髒堆疊資訊,導致奇奇怪怪難以排查的bug
  • 可以更多的使用模板超程式設計,儘量多的使用constexpr等編譯器計算,編譯器是我們的好搭檔,個人認為模板超程式設計以後會是C++的主流技術
  • 可以考慮更多的使用異常處理方式,而不是C語言風格的errno錯誤碼等,這裡可以參考你的c++團隊還在禁用異常處理嗎?

附:本文不是技術文章,介紹較為主觀,可能和很多人想法有所衝突,各位可以結合自己的經歷經驗酌情參考。

參考資料