最近開始接手項目組的開發管理工作,項目組開發的產品一期功能基本開發完成,進入內部測試及小渠道發布階段,然而產品的穩定性還存在很大問題。
先做一下背景介紹,項目組開發的是一款面向C端的互聯剛產品,運行操作系統為windows。整個項目使用c++開發,總體代碼量大概在數十萬行。
c++是一門很復雜的語言,有很多強大的特性,然而當用其開發一款商業產品時,這些特性可能會帶來麻煩。所以當設計c++的使用規范時,更多的是對其做減法。
本文的規范針對VC++開發環境,開發工具為Visual Studio。
文件系統目錄規范
一款完整的商業產品開發通常會涉及到很多模塊,這其中包括可執行程序(.exe),項目組開發的庫(靜態庫或動態庫),第三方的庫(靜態庫或動態庫),測試程序,這么多的模塊和代碼,需要一個良好組織的目錄結夠。
這里假設項目名稱為XXProject
- XXProject
- XXProject.sln
- Bin
- Debug
- Release
- TestBin
- Debug
- Release
- Src
- Document
其中Bin存放需要發布的可執行程序,TestBin存放測試程序的可執行文件,Src存放項目的工程文件和源代碼,Document存放項目相關的開發文檔(如項目說明,代碼規范等)。
接下來假設項目包括如下工程:XXMain(發布的主程序),XXUpdate(升級程序),XXSdk(自己開發的基礎庫),XXThird(第三方庫), XXTest(測試程序)
- Src
- XXMain
- XXMain.vcxproy
- code文件
- XXUpdate
- XXUpdate.vcxproy
- code文件
- XXTest
- XXTest.vcxproy
- code文件
- XXSdk
- XXSdk.vcxproy
- code文件
- ThirdLib
- XXThird
- Lib
- Debug
- Release
- Include
Lib庫用來存放靜態庫,動態庫的.a文件,Include用來存放公共頭文件,ThirdLib用來存放第三方庫。
解決方案目錄規范
解決方案目錄是VS開發工具提供的邏輯上組織項目的方式,與物理文件系統并不存在對應關系。
仍然假設項目包含上述項目和模塊。
- Solution (解決方案)
- Application (解決方案文件夾)
- XXMain (工程)
- XXUpdate (工程)
- Test (解決方案文件夾)
- XXTest (工程)
- Library (解決方案文件夾)
- XXSdk
- ThirdLibrary(解決方案文件夾)
- XXThird
- Public (解決方案文件夾)
- 公共頭文件
代碼編寫規范
1:禁用全局變量
全局變量會帶來晦澀的依賴問題
2:禁用goto指令
goto指令的代碼難以閱讀和維護
3:禁用異常機制
c++的異常機制有很多缺陷且復雜
4:用struct封裝數據,使用class定義對象
C++中class和struct幾乎沒有區別,在規范中進行語義的區分
5:struct和class必須顯示包含構造函數
6:除非特殊情況,總是將析構函數定義為虛函數
方便繼承時的資源釋放
7:不要在構造函數中執行復雜操作,推薦加入init函數用于初始化操作
構造函數沒有返回值,難以反饋錯誤
8:不要在構造函數中調用虛函數
構造函數中的虛函數不會重定向到子類。
9:慎用繼承
相比對象組合,繼承帶來更強的依賴,推薦使用接口繼承而不是對象繼承
10:禁用多重繼承(接口繼承除外)
多重繼承通常代表不好的設計
11:慎用運算符重載
運算符重載會混淆代碼的語義,應只在不會造成混淆時使用
12:將成員設置為私有并提供訪問函數
封裝是降低代碼耦合的有力武器
13:將同一訪問權限的成員定義在一起
可以按照public,protect,private順序進行組織
14:避免出現大而全的類
當一個類的代碼超過1000行,應有所警惕,超過2000行,則應考慮拆分(行數不包括注釋)
15:頭文件應包含它所需要的頭文件
這樣可以保證cpp文件引入該頭文件后不需要包含其它頭文件
16:合理的組織引入的頭文件,不要重復引入,不要引入不必要的頭文件
可以以系統頭文件,第三方庫頭文件,項目組庫頭文件,本程序頭文件來組合,不同類型頭文件之間用空格隔開
17:頭文件使用#define宏來避免多重包含
pragma once 指令只有VC編譯器能識別
18:允許合理的使用友元特性
19:使用引用傳遞對象類型參數, 對于不需要改變的參數加入const修飾符
引用傳遞可以避免對象拷貝
20:函數應該進參在前,出參在后
21:使用明確的返回值指示函數的運行結果,而不是用返回的內容來指示結果
推薦: int GetDeviceName(string& deviceName);
不推薦: string GetDeviceName()
22:聲明基本類型變量后立即賦值
正確 int nCount = 0; bool bSuc = false; 錯誤 int nCount; bool bsuc;
23:使用內聯,枚舉,常量來代替宏
宏的使用有很多弊端,應盡量避免
24:使用singleton模式代替靜態類
相比靜態類,singleton模式可以更好地控制初始化時機。
25:使用share_ptr來管理指針
指針和管理在復雜項目中十分困難,使用智能指針是不二選擇
26:使用weak_ptr來處理循環引用
27:明確對象或資源的生存周期
明確對象的生存周期通常代表著良好的設計
28:合理的使用縮進,空格
最重要的是保持風格的統一,自動生成的代碼可能會打破這種統一,應該靈活設計規則
29:合理使用typedef縮減類型的長度,合理使用auto
使用stl時經常會導致過長的類型,合理使用auto可以有效減少代碼長度
命名規則
由于是在VC環境下開發,沿用微軟的命名駝峰命名法
選擇哪種命名方式實際上不是很重要,最重要的是保持統一
- 代碼文件: DeviceMgr.h, DeviceMgr.cpp
- 解決方案目錄: NetLibrary
- 工程篩選器: DataModel
- 類:CDeviceMgr
- 結構體: DeviceInfo
- 變量: listDevice
- 類成員: m_deviceName
- 函數: GetName; GetDeviceName;
- 代表bool含義的變量: 都以b開頭,如:bOk, bSafe
- 代表整數含義的變量: i表示符號整數,n表示無符號整數
- 避免無意義的變量名和縮寫: 如 x,dn(deviceName)等
注釋規則
- 文件注釋:注明文件作者,聯系方式,文件代碼作用,重大修改記錄等
- 類注釋:說明類的作用,使用限制等等
- 函數注釋: 盡量依靠意義明確的函數命名而不是依靠注釋,說明函數的使用限制,對于意義不明確的參數加以說明
- 變量注釋:盡量依靠意義明確的變量命名而不是依靠注釋,特殊情況。
- 實現注釋: 對于使用了非常規技巧,或復雜算法,或很復雜的業務邏輯部分要加入注釋說明
原則: 注釋應風格統一,簡短而意義明確,最終目的是有效幫助其它人閱讀和理解代碼的目的
日志
日志的打印十分重要,是產品發生問題時重要的參考依據
- 日志的打印要盡量詳盡,合理劃分日志等級,一般為Fatal, ERROR, WARNING, DEBUG, INFO
- 統一使用unicode(utf-16)編碼來輸出日志
- 提供異步打印日志的接口
- 提供定期清理日志的機制
- 在發布版中將日志等級設置為ERROR或更高,提供配置文件供調整日志等級
一些其它規則
- 避免大而全的類(代碼控制在1000行以內)
- 避免過長的函數(代碼控制在200行以內)
- 避免深層的嵌套(不要超過3層)
- 使用do-while-break技巧來避免重復寫釋放資源的代碼
- 盡量使用RALL技巧來釋放資源
- 當函數或代碼廢棄時,應與標注,最好將其注釋掉并定期清理
線程和鎖
復雜的項目肯定會涉及到多線程開發,而開發多線程程序是十分困難的。據我們統計,項目組產品有70%左右的崩潰和bug和線程有關。
將線程模塊獨立出來,交給項目最有經驗的開發人員管理和維護,對外暴露抽象接口,屏蔽線程的概念。
強制使用RALL技術來使用鎖
未盡
本文并沒有涉及到C++規范的所有方面,歡迎討論和補充