項目組一直沒有做代碼審查,最近有啟動這項計劃的打算,因此提前複習一下《C++編程規範》,並做一些筆記。我們做任何事通常都先從簡單的入手,循序漸進,持續改進,那麼做代碼審查也不例外,《C++編程規範》又很多,如果一下子突然引入,會對代碼編寫提出過高的要求,對開發人員的打擊比較大,從而可能會影響團隊的整個士氣,所以我想我們應該從最簡單(即容易遵循做到)、最重要的幾個規範開始,即追求 【有效性/複雜性】 最大化。
聯想到排程的十字表格,如法炮製了如下表格,以便分門別類:
代碼審查
| B(簡單&很重要) |
C(複雜&很重要) |
| A(簡單&較重要) |
D(複雜&較重要) |
當然,規範本沒有重要與不重要之分,這裡這樣給其畫上這樣的標籤,只是一個相對的概念,是給我們推進“代碼審查”這項工作一個簡單的指引,例如先實施A區的規則,一段時間後,當團隊成員都習慣了這些規則,再實施B區的規則,基本上按照先易後難的順序,依次推進。
由於筆者的學識水平,以及經驗所致,對一些規則的認識肯定存在偏差與不妥,還請同學批評賜教。
《C++編程規範——101條規則、準則與最佳實務》(C++ Coding Standards——101 Rules, Guidelines and Best Practices)
組織和策略問題
第0條(D): 不要拘泥於小節(又名:瞭解哪些東西不應該標準化)
是的,有些東西不應該規定過死。但是我們認為,在一些個人風格和喜好方面,保持團隊內部的一致性是有好處的。一致性的重要性似乎怎麼強調都不過分。
第1條(B):在高警告層級下乾淨利落地進行編譯
將編譯器的警告層級調到最高,高度重視警告。編譯器是我們的好朋友,如果它對某個構建發出警告,就說明這個構建可能存在潛在的問題。這個問題可能是良性的,也可能是惡性的。我們應該通過修改代碼而不是降低警告層級來消除警告。
第2條(C):使用自動構建系統
“一鍵構建”,甚至使構建伺服器根據代碼的提交自動進行構建,持續整合,不斷交付軟體產品,在迭代中完善。儘管現在有了很多開源的自動化構建系統,但是搭建並維護這樣一個自動化構建系統,需要團隊中有一位經驗豐富的高手。
第3條(B):使用版本控制系統(VCS)
VSS, SVN, GIT。
第4條(*):做代碼審查
審查代碼:更多的關注有助於提高品質。亮出自己的代碼,閱讀別人的代碼。互相學習,彼此都會受益。代碼審查無需太形式主義,但一定要做,團隊可根據自己的實際情況嘗試著去做,在做的過程中,慢慢改進,找到一個符合團隊實際需要的方式。CppCheck
設計風格
第5條(C):一個實體應該只有一個緊湊的職責
一次只解決一個問題:只給一個實體(變數、類、函數、名字空間、模組和庫)賦予一個定義良好的職責。隨著實體變大,其職責範圍自然也會擴大,但是職責不應該發散。
第6條(C):正確、簡單和清晰第一
軟體簡單為美(KISS原則:Keep It Simple Software):品質優於速度;簡單優於複雜;清晰優於機巧;安全第一。可讀性:代碼必須是為人編寫的,其次才是電腦。
第7條(C):編程中應該知道何時和如何考慮延展性
面對資料的爆炸性增長,應該集中精力改善演算法的O(N)複雜度。在這種情況下,小型的最佳化(例如節約一個賦值、加法或乘法運算)通常無濟於事。
第8條(A):不要進行不成熟的最佳化
第9條(A):不要進行不成熟的劣化
構造既清晰又有效程式有兩種方式:使用抽象(DIP)和庫(STL)。
第10條(B):盡量減少全域和共用資料
第11條(C):隱藏資訊
第12條(D):懂得何時和如何進行並發性編程
安全執行緒?並發編程?加鎖解鎖死結?這些對我來說還只是屬於概念......,鄙視一下自己!
第13條(B):確保資源為對象所擁有。使用顯式的RAII和智能指標
利器在手,不要再徒手為之。當然,也要防止智能指標的過度使用。如果只對有限的代碼可見(例如函數內部,類內部),原始指標就夠用了。
編程風格
第14條(C):寧要編譯時間和串連時錯誤,也不要執行階段錯誤
第15條(A):積極的使用const
const是我們的朋友,不變的值更易於理解,跟蹤和分析。定義值的時候,應該將const作為預設選項。用mutable成員變數實現邏輯上的不變性,在給某些資料做緩衝處理的時候經常使用這一特性。即在類的const成員函數中可以合法的修改類的mutable成員變數。
當然,對於那種通過值傳遞的函數參數聲明為const 純屬多此一舉,反而還會引起誤解。
第16條(D):避免使用宏
這似乎是C++中一條人人皆知的編程規範。可真正嚴格遵守的團隊並不多(純屬猜想,哈哈)。
第17條(D):避免使用“魔數”
同第16條。
應該用符號常量替換直接寫死的字串(或宏)。將字串與代碼分開(比如將字串放入一個獨立的CPP檔案中),這樣有利於審查和更新,而且有助於國家化。
第18條(A):儘可能局部地聲明變數
避免範圍膨脹。變數的生存期越短越好。因此,儘可能只在首次使用變數之前聲明之(通常這時你也有足夠的資料對它初始化了)。
第19條(B):總是初始設定變數。
這裡有兩段很好的範例程式碼:
// 雖然正確但不可取的方式:定義變數時沒有初始化int speedupFactor;if (condition) speedupFactor = 2;else speedupFactor = -1;
以下兩種方式更好一些:
// 可取的方式一:定義變數時即初始化int speedupFactor = -1;if (condition) speedupFactor = 2; // 較好且簡練的方式二:定義變數時即初始化int speedupFactor = condition ? 2 : -1;
第20條(D):避免函數過長,避免嵌套過深
第21條(C):避免跨編譯單元的初始化依賴
第22條(A):盡量減少定義性依賴。避免循環相依性。
儘可能的使用前置聲明(forward declaration). Pimpl慣用法對遵循這一規範有實際性的協助。DIP(依賴倒置原則)。
第23條(A):標頭檔應該自給自足
各司其責:應該確保每個標頭檔都能夠單獨編譯。
第24條(A):總是編寫內部的#include保護符,決不要編寫外部的#include保護符
函數與操作符
第25條(B):正確地選擇通過值、(智能)指標或者引用傳遞參數
第26條(C):保持重載操作符的自然語義
第27條(C):優先使用算術操作符和賦值操作符的標準形式
1)如果要定義 a+b,也應該定義 a+=b。一般利用後者實現前者,即賦值形式的操作符完成實際的工作,非賦值形式的操作符調用賦值形式的操作符。2)如果可能,優先選擇將這些操作符函數定義為非成員函數,並將其和要類型T放入同一個名字空間中。3)非成員函數傳回值或引用,成員函數返回引用。
帖幾段範例程式碼:
// 成員函數 @= T& operator@=(const T& rhs){ //.....具體的實現代碼..... return *this;}// 非成員函數 @ T operator@(const T& lhs, const T& rhs){T temp = lhs;return temp @= rhs;}// 非成員函數 @= 返回輸入參數的引用T& operator@=(T& lhs, const T& rhs){ // ......具體的實現代碼...... return lhs; // 返回輸入參數的引用}// 非成員函數 @ T operator@(T lhs, const T& rhs){ return lhs @= rhs;}
第28條(A):優先使用++和--的標準形式。優先使用首碼形式
如果定義了++C,也應該定義 C++,而且應該用首碼形式實現尾碼形式。
標準形式,範例程式碼:
// 首碼形式T& operator++(){ // ......執行遞增的實現代碼 return *this; // 返回遞增後的新值} // 尾碼形式T operator++(int) // 傳回值{ T oldT(*this); // 先儲存原值 ++(*this); // 調用首碼形式執行遞增 return oldT; // 返回原值}
第29條(D):考慮重載以避免隱含類型轉換
第30條(A):避免重載操作符&&, || 和,(逗號)
第31條(A):不要編寫依賴於函數參數求值順序的代碼
調用函數時,參數的求值順序是懸而未定的。