范實戰(zhàn):從代碼布局到錯誤處理的團隊落地指南)
簡介《中興通訊軟件編程規(guī)范——C/C》是中興通訊股份有限公司2003年發(fā)布的企業(yè)設(shè)計技術(shù)標準面向所有使用C/C語言進行軟件開發(fā)的工程師用于統(tǒng)一公司內(nèi)部軟件編程風格提升源程序的可讀性、可靠性和可維護性。這份規(guī)范按規(guī)則性和建議性兩類要求組織內(nèi)容涵蓋基本原則、布局、注釋、命名規(guī)則、變量常量與類型、表達式與語句、函數(shù)與過程、可靠性、可測試性、斷言與錯誤處理等12個章節(jié)附錄A和附錄B還提供編程模板與規(guī)范檢查表便于開發(fā)者在實際項目中逐項對照執(zhí)行。壓縮包為1個doc格式文檔容量僅486KB輕量聚焦適合隨時查閱。該文檔已有301人學習瀏覽既能幫助企業(yè)團隊快速建立統(tǒng)一的編碼標準也適合個人開發(fā)者自查代碼風格、減少缺陷是一份頗具參考價值的經(jīng)典企業(yè)級編程規(guī)范資料。1. 一份C/C編碼規(guī)范憑什么值得你逐行讀完接手一個維護了三年多的嵌入式通信項目我第一次打開核心模塊源碼的時候心態(tài)是崩潰的一個文件里三種縮進風格變量名從a1、tmp到m_pStrCount混著來函數(shù)長度三百行起步最要命的是沒有任何注釋說明這段邏輯到底在干什么。后來項目組引進了一份內(nèi)部的C/C編碼規(guī)范要求所有新代碼和改動代碼必須按它執(zhí)行。三個月后代碼審查效率明顯提升新人上手時間縮短線上故障排查也輕松了不少。這份規(guī)范就是某通信設(shè)備公司于2003年發(fā)布的企業(yè)標準《軟件編程規(guī)范——C/C》編號 Q/ZX 04.302.1–2003。它覆蓋了布局、注釋、命名、表達式、函數(shù)、可靠性、可測試性和錯誤處理適合C/C開發(fā)工程師、嵌入式軟件團隊以及想建立代碼規(guī)范的技術(shù)負責人閱讀。今天我把這份規(guī)范拆開結(jié)合實戰(zhàn)場景講講它到底怎么用。2. 設(shè)計原則與文件布局先統(tǒng)一思想再統(tǒng)一代碼規(guī)范的價值從來不是約束而是統(tǒng)一認識。這一章是整套規(guī)范的思想底座之后所有章節(jié)的規(guī)則幾乎都是從這幾個原則推演出來的值得逐條細品。2.1 基本原則這八條守則是整份規(guī)范的思想根基規(guī)范開篇提出八條基本原則寫得很樸素但每一條都對應(yīng)著實際開發(fā)中的典型問題。【原則1-1】“首先是為人編寫程序其次才是計算機”——初看像句口號但仔細想代碼要經(jīng)歷開發(fā)、測試、生產(chǎn)、用戶使用、版本升級和后期維護只有人能順利讀懂的代碼才談得上維護。很多團隊翻車不是因為功能沒實現(xiàn)而是因為三個月后沒人敢改那段“高效但晦澀”的代碼。【原則1-2】“保持代碼的簡明清晰避免過分追求技巧”——過度使用宏、表達式里堆疊運算符、為了少寫幾行把邏輯壓縮成一行這些都是常見的反面教材。簡明清晰就是把意圖直接表達出來而不是讓讀者做閱讀理解?!驹瓌t1-3】“所有代碼盡量遵循ANSI C標準”——這一條在2003年的背景下尤其重要當時的編譯器各有各的擴展語法。盡量不用未定義行為或編譯器擴展是為了可移植性。【原則1-4】“首先達到正確性其次考慮效率”——這條順序太關(guān)鍵了。我見過太多人一上來就優(yōu)化循環(huán)、改用寄存器變量結(jié)果功能都沒跑對。正確性、健壯性、可維護性先過關(guān)性能最后調(diào)這個次序不能倒?!驹瓌t1-5】“避免或少用全局變量”——全局變量帶來的問題是模塊間緊耦合一處改動處處受牽連。規(guī)范不搞一刀切而是說“少用”這符合工程實際?!驹瓌t1-6】“盡量避免使用GOTO語句”——GOTO會讓程序的控制流變成意大利面條不到萬不得已不用。這條原則在后續(xù)的“表達式與語句”章節(jié)會用規(guī)則進一步約束。【原則1-7】“盡可能重用、修正老的代碼”——不是重新發(fā)明輪子而是對已驗證的代碼做修改優(yōu)化來滿足新需求減少測試成本和質(zhì)量風險。【原則1-8】“盡量減少同樣的錯誤出現(xiàn)的次數(shù)”——復(fù)盤歷史缺陷把常見錯誤沉淀成規(guī)范條目這就是為什么規(guī)范越積越厚的原因。2.2 文件布局頭文件和實現(xiàn)文件的書寫順序規(guī)范對頭文件和實現(xiàn)文件的布局給出了明確的章節(jié)順序這一點對讀老代碼特別有幫助——你打開一個文件就能知道哪個區(qū)塊該找什么。頭文件布局順序是文件頭注釋、#ifndef防止重復(fù)包含、#include依次放標準庫頭文件、非標準庫頭文件、常量定義、全局宏、全局數(shù)據(jù)類型、類定義模板、全局函數(shù)原型、#endif。實現(xiàn)文件布局順序是文件頭注釋、#include、常量定義、文件內(nèi)部使用的宏、文件內(nèi)部使用的數(shù)據(jù)類型、全局變量、本地變量靜態(tài)全局變量、局部函數(shù)原型、類的實現(xiàn)、全局函數(shù)、局部函數(shù)。這里我實際用下來最受益的是兩條硬性規(guī)則。第一條是【規(guī)則2-1-4】頭文件必須避免重復(fù)包含用宏定義保護#ifndef MODULE_H #define MODULE_H /* 文件體 */ #endif這個寫法現(xiàn)在已經(jīng)是常識了但在老代碼里依然能看到不寫保護的頭文件一旦被兩個源文件包含就會報重復(fù)定義錯誤。第二條是【規(guī)則2-1-5】標準庫頭文件用尖括號非標準庫用雙引號這個約定讓閱讀者一眼分辨頭文件的來源歸屬。2.3 文件布局的價值給代碼建立“心理地圖”把布局規(guī)則落到工程實踐上本質(zhì)上是給代碼建立一套心理地圖。比如我后來接手別人的模塊先是翻文件頭注釋了解用途和修改記錄然后掃常量定義和數(shù)據(jù)類型再看全局函數(shù)原型基本不用通讀全文就能知道這個文件的“骨架”。這樣做的好處是代碼審查時能快速定位到關(guān)鍵節(jié)點缺陷排查時能判斷哪個區(qū)域最可能出問題。3. 從布局到注釋可讀性不是靠感覺是靠規(guī)則布局是代碼的外殼注釋則是代碼的說明書。這一章把規(guī)范里最詳盡的布局規(guī)則和注釋規(guī)則拆開講每一條都有正反例可以直接當團隊checklist用。3.1 基本格式與縮進四個空格的執(zhí)念規(guī)范對格式的要求極其具體不搞“建議盡量”這種軟話直接就是規(guī)則?!疽?guī)則2-2-1】一行代碼和注釋不能超過80列包括空格在內(nèi)。這條約束寫代碼的人要時刻注意斷行否則在分屏對比代碼時會被折行搞瘋?!疽?guī)則2-2-2】if、else、else if、for、while、do等語句自占一行執(zhí)行語句不得緊跟其后不論執(zhí)行語句有多少都要加{}。/* 正例 */ if (varible1 varible2) { varible1 varible2; } /* 反例執(zhí)行語句緊跟if條件且不加花括號 */ if (varible1 varible2) varible1 varible2;這條規(guī)則我舉雙手贊成。不加花括號的寫法在后續(xù)維護中極易埋雷——有人想往分支里加一行代碼結(jié)果忘了加花括號新代碼變成了無條件執(zhí)行。這是典型的“省一行毀一夜”翻車現(xiàn)場。【規(guī)則2-3-1】禁止使用TAB鍵必須使用空格進行縮進縮進為4個空格。原因很簡單不同的編輯器對TAB的解析寬度不一樣在A機器上對齊的代碼到B機器上就錯位了。現(xiàn)在IDE基本都能配置“TAB轉(zhuǎn)空格”但在老舊的嵌入式開發(fā)環(huán)境里這條規(guī)則還是能避免大量對齊問題。實戰(zhàn)中我建議在項目根目錄放一份.editorconfig文件把indent_style space和indent_size 4固定下來?!疽?guī)則2-3-2】程序的分界符{和}應(yīng)獨占一行并且位于同一列同時與引用它們的語句左對齊。這樣寫的好處是成對的花括號垂直對齊代碼塊邊界一目了然。3.2 對齊、空行與空格讓代碼呈現(xiàn)“結(jié)構(gòu)感”布局規(guī)則有很多細節(jié)但它們都在為同一個目標服務(wù)讓代碼呈現(xiàn)出結(jié)構(gòu)感讓人眼能像看報紙一樣掃讀。【規(guī)則2-3-5】相關(guān)的賦值語句等號對齊。舉規(guī)范里的例子tPDBRes.wHead 0; tPDBRes.wTail wMaxNumOfPDB - 1; tPDBRes.wFree wMaxNumOfPDB; tPDBRes.wAddress wPDBAddr; tPDBRes.wSize wPDBSize;一組同類的賦值語句對齊后右側(cè)的變量值和左側(cè)的邏輯關(guān)系一目了然。維護時增刪字段也能立刻發(fā)現(xiàn)不對稱的地方?!疽?guī)則2-4-1】不同邏輯程序塊之間要用空行分隔。函數(shù)與函數(shù)之間、循環(huán)體與后續(xù)代碼之間這些位置都應(yīng)該有空行讓代碼“喘口氣”。void Foo::Hey(void) { /* Hey實現(xiàn)代碼 */ } /* 空一行 */ void Foo::Ack(void) { /* Ack實現(xiàn)代碼 */ }【規(guī)則2-4-2】【規(guī)則2-4-3】在運算符空格上做了標準化一元操作符如!、~、、--、*、地址運算符前后不加空格多元運算符和操作數(shù)之間至少一個空格。/* 一元運算符 */ !bValue ~iValue iCount *strSource fSum /* 多元運算符 */ fValue fOldValue; fTotal fValue; iNumber 2;【規(guī)則2-4-4】關(guān)鍵字之后要留空格。if、for、while等關(guān)鍵字后應(yīng)留一個空格再跟左括號以突出關(guān)鍵字與函數(shù)調(diào)用的區(qū)別。函數(shù)名后則不要留空格【規(guī)則2-4-5】這樣一眼就能區(qū)分if (和DoSomething(。3.3 斷行規(guī)則長表達式怎么拆才不破壞邏輯超過80列時必須斷行但怎么斷有講究?!疽?guī)則2-5-1】長表達式要在低優(yōu)先級操作符處拆分成新行操作符放在新行之首以便突出操作符。拆分出的新行要做適當縮進。/* 條件表達式續(xù)行在第一個條件處對齊 */ if ((iFormat CH_A_Format_M) (iOfficeType CH_BSC_M)) { DoSomething(); } /* 賦值語句的續(xù)行在賦值號處對齊 */ fTotalBill fTotalBill faCustomerPurchases[iID] fSalesTax(faCustomerPurchases[iID]);注意看操作符和都放在了新行的行首這樣讀代碼時先看到操作符再看到操作數(shù)邏輯重心在前可讀性確實更好。3.4 注釋規(guī)則注釋是解釋意圖不是復(fù)述代碼規(guī)范的注釋章節(jié)開篇就說了一段很有價值的話有效的注釋是指在代碼的功能、意圖層次上進行注釋提供有用、額外的信息而不是代碼表面意義的簡單重復(fù)?!疽?guī)則3-1】C語言的注釋符為/* ... */C語言中多行注釋采用/* ... */單行注釋采用// ...?!疽?guī)則2-4-7】注釋符與注釋內(nèi)容之間要用一個空格進行分隔。/* 注釋內(nèi)容 */ // 注釋內(nèi)容我自己的習慣是文件頭寫版權(quán)聲明和修改歷史函數(shù)定義上方寫功能、參數(shù)、返回值和注意事項代碼行內(nèi)注釋只加在有隱含邏輯和魔法數(shù)字的地方。一眼能看懂的代碼不注釋看不懂的代碼才需要注釋。4. 命名、變量與函數(shù)給機器寫的代碼給人命名命名是編程里最難的事之一。規(guī)范用了一整章的篇幅定義命名規(guī)則并且給出了一套實用的前綴體系。這一章把命名規(guī)則和變量、函數(shù)的規(guī)范串起來講落到實際怎么寫。4.1 命名規(guī)則與變量前綴從名字就能讀出“元信息”規(guī)范的命名規(guī)則章節(jié)定義了完整的匈牙利命名法變體。變量名要帶類型前綴指針用p函數(shù)指針用fp全局變量用g靜態(tài)變量用s類成員變量用m_。拿規(guī)范里的例子看float *pfBuffer; /* p表示指針f表示floatBuffer是用途 */ int iCount; /* i表示int */ char chKey; /* ch表示char */ BYTE ucCallNo; /* uc表示unsigned char */這套命名法的精髓在于從變量名就能讀出它的類型、作用域不用回頭翻定義?!疽?guī)則2-2-3】定義指針類型的變量*應(yīng)放在變量前寫成float *pfBuffer而不是float* pfBuffer。這個細節(jié)起初看起來有點別扭但讀代碼的時候“指針屬性”緊貼變量名效果更直觀。為了避免變量名過長或過短命名規(guī)則章節(jié)還要求不使用含義模糊的縮寫。比如用iLength而不是iLen用strUserName而不是strUN。我見過太多用tmp1、tmp2糊弄的代碼三個月后自己都分不清哪個是哪個。類定義內(nèi)部【規(guī)則2-3-3】public、protected、private關(guān)鍵字要與{}對齊這些部分的內(nèi)容要縮進。類成員變量統(tǒng)一加m_前綴避免與函數(shù)局部變量混淆class CCount { public: CCount(void); ~CCount(void); int GetCount(void); void SetCount(int iCount); private: int m_iCount; };4.2 變量、常量與類型全局變量嚴格控制規(guī)范對變量的約束集中在“少用全局變量”這個原則上具體落地包括模塊內(nèi)全局變量用static修飾限制可見范圍定義常量用const而不是#defineC環(huán)境下枚舉類型用于有窮集合的取值。類型定義上有兩條硬規(guī)則結(jié)構(gòu)體類型定義要放在頭文件或文件前部的“數(shù)據(jù)類型定義”區(qū)塊中復(fù)雜的類型定義必須寫注釋說明其用途和約束。這些都是小細節(jié)但加在一起能顯著減少誤用和誤讀。4.3 表達式、語句與函數(shù)用規(guī)則把“坑”填平【規(guī)則2-2-2】已經(jīng)要求if語句強制執(zhí)行體加大括號函數(shù)章節(jié)則對函數(shù)本身的寫法提出了約束。函數(shù)實現(xiàn)遵循單一職責的原則一個函數(shù)只做一件事函數(shù)的行數(shù)控制在50行以內(nèi)建議性規(guī)范。函數(shù)名用動詞開頭如GetCount()、SetCount()、DoSomething()。函數(shù)參數(shù)的書寫有明確規(guī)則——【規(guī)則2-5-2】函數(shù)聲明時類型與名稱不允許分行書寫/* 正例 */ extern double FAR CalcArea(double dWidth, double dHeight); /* 反例類型與名稱被拆到兩行 */ extern double FAR CalcArea(double dWidth, double dHeight);函數(shù)聲明時類型與名稱同行參數(shù)過多時在第一個參數(shù)處對齊續(xù)行。這樣寫讓函數(shù)簽名在代碼搜索和IDE解析時更穩(wěn)定尤其是在老式的源碼瀏覽工具里折行會破壞函數(shù)原型的解析。4.4 實際應(yīng)用代碼審查時怎么用這套規(guī)則我在團隊里推這套命名規(guī)范時有個體會與其靠人自覺不如把規(guī)范變成代碼審查的檢查項。審查時先看文件名和頭文件布局再看變量命名是否帶合法前綴最后看函數(shù)長度是否超標。自動化工具如cppcheck和clang-tidy能檢查一部分格式問題但命名語義和前綴規(guī)則還是得靠人眼。5. 可靠性、可測試性與錯誤處理避坑與排查實戰(zhàn)前面幾章講的都是代碼“長什么樣”這一章要講的是代碼“要不出事”。規(guī)范和錯誤處理章節(jié)直接關(guān)系線上質(zhì)量是這次拆解的重點也是踩坑記錄最豐富的部分。5.1 可靠性從衛(wèi)生習慣開始可靠性章節(jié)的核心思路是“防御性編程”假設(shè)輸入是惡意的、假設(shè)資源是有限的、假設(shè)外部環(huán)境是不可靠的。具體規(guī)則包括函數(shù)入口處檢查參數(shù)合法性指針使用前判空除法運算前檢查分母不為零數(shù)組訪問前檢查索引邊界申請的內(nèi)存一定要釋放且成對出現(xiàn)。這些都是教科書寫爛了的內(nèi)容但真正能在每一行代碼里堅持的人不多。另一個重要條目是防止整數(shù)溢出。在計算緩沖區(qū)長度或分配大小時先判斷加法和乘法是否溢出再執(zhí)行操作這是很多安全漏洞的根源。規(guī)范在2003年就寫了不要使用無符號整數(shù)的循環(huán)條件因為for (unsigned int i n; i 0; i--)是一個死循環(huán)——i減到0后再減會變成最大值這個坑我親眼見過有人踩排查了大半天。5.2 可測試性代碼從設(shè)計上就要容易被驗證可測試性章節(jié)建議函數(shù)盡量不依賴全局狀態(tài)輸入輸出盡量通過參數(shù)和返回值傳遞模塊通過接口解耦便于打樁和模擬。這在當時是相當先進的思想——很多團隊還以為“測試”就是編譯運行看結(jié)果。規(guī)范附錄A的編程模板提供了標準的代碼組織格式從文件頭到函數(shù)實現(xiàn)都有空行和注釋位附錄B的規(guī)范檢查表則是一個逐項打勾的清單。這兩份附錄是直接把規(guī)范落地的實操工具而不是停留在概念層面。5.3 斷言與錯誤處理程序要“死得明白”斷言與錯誤處理章節(jié)強調(diào)斷言用于捕獲“不應(yīng)該發(fā)生”的程序邏輯錯誤錯誤處理用于處理“可能發(fā)生”的運行時異常。兩者分工明確不能混淆。/* 斷言示例 */ assert(pBuffer ! NULL); /* 錯誤處理示例 */ if (pBuffer NULL) { return ERROR_NULL_POINTER; }使用斷言的一個重要準則是斷言只應(yīng)在調(diào)試版本中生效發(fā)布版本要把斷言編譯掉。如果斷言保留在發(fā)布版本中可能會因為觸發(fā)斷言而暴露內(nèi)部信息甚至被利用來發(fā)起拒絕服務(wù)攻擊。錯誤處理的三條原則是錯誤信息要包含足夠的上下文哪個模塊、哪個操作錯誤碼要有統(tǒng)一的定義和管理資源清理要做“對等釋放”——在函數(shù)的所有退出路徑上都要釋放資源包括正常返回和錯誤返回。最常見的資源泄漏是提前return時忘了釋放這類問題用cppcheck能自動查出來一部分但最好從代碼審查源頭解決。5.4 常見踩坑記錄以下是我在實踐中整理的高頻問題每一條都是真實發(fā)生過的按“現(xiàn)象→原因→解決”的套路寫出來。踩坑一TAB和空格混用導(dǎo)致代碼錯位現(xiàn)象代碼在開發(fā)者的編輯器里對齊整齊提交到代碼庫后其他人打開看全是錯位的。原因有人用TAB縮進有人用4個空格縮進而不同編輯器對TAB的渲染寬度不同。解決項目根目錄統(tǒng)一放.editorconfig強制indent_style space、indent_size 4代碼提交前跑一遍格式檢查腳本不符合的直接拒絕提交。踩坑二匈牙利前綴誤導(dǎo)類型判斷現(xiàn)象代碼里有個變量叫iYear結(jié)果實際是個unsigned int還有人底層是char的數(shù)組存年份。原因命名時沒嚴格綁定類型前綴后面類型改了但名字沒同步。解決把命名列入代碼審查硬性檢查項類型變更時變量名同步修改。雖然改起來麻煩但不改的后患更大。踩坑三#include頭文件順序混亂導(dǎo)致編譯失敗現(xiàn)象自己本地編譯過了別人一拉代碼就報錯換個編譯器又報錯。原因頭文件包含順序依賴了編譯環(huán)境的隱式包含非標準頭文件寫在了標準庫前面導(dǎo)致宏定義或類型定義缺失。解決嚴格執(zhí)行【規(guī)則2-1-5】的標準庫在前、非標準庫在后的順序同時頭文件加保護宏。這樣依賴關(guān)系穩(wěn)定編譯錯誤也容易定位。踩坑四斷言在發(fā)布版本中對用戶彈錯誤框現(xiàn)象程序發(fā)布后用戶操作時突然彈出一個斷言失敗對話框。原因斷言沒有被編譯掉錯誤處理被當成了斷言來寫。解決區(qū)分斷言和錯誤處理斷言只檢查“不可能發(fā)生”的條件編譯時排除在發(fā)布版本外用戶可見的錯誤路徑一律用錯誤處理代碼。踩坑五長表達式斷行位置不對邏輯被讀錯現(xiàn)象后來的人修改條件表達式時誤以為換行后的兩行是并列的兩個條件。原因斷行時沒有把操作符放在行首或者縮進不對讓閱讀者產(chǎn)生了歧義。解決嚴格按【規(guī)則2-5-1】操作符放行首續(xù)行縮進。這屬于格式問題但格式問題直接影響人腦對邏輯的判斷。6. 把規(guī)范落地到自己的項目從檢查表到漸進式改造這可能是讀者最關(guān)心的一章我怎么把這套2003年的規(guī)范用在自己的項目上而不是把它鎖進抽屜里。我的經(jīng)驗是別指望一步到位用漸進式改造和工具化手段讓規(guī)范真正在一個存量代碼庫里生根。第一步是引入規(guī)范檢查表。規(guī)范附錄B本來就是逐項打勾的檢查表我把它翻譯成團隊代碼審查的模板文件頭是否完整、頭文件保護宏是否存在、縮進是否4空格、變量名前綴是否合法、函數(shù)行數(shù)是否超標、空行是否分隔邏輯塊、斷行是否合規(guī)。審查者在代碼評審時逐項過一遍幾分鐘就能完成。這三五分鐘的成本換來的是代碼庫的長期一致性性價比很高。第二步是格式化工具先行。用clang-format把布局規(guī)則自動化配置項就按規(guī)范來ColumnLimit: 80、IndentWidth: 4、UseTab: Never、BreakBeforeBinaryOperators: All。人工審查只管邏輯和命名格式問題交給工具。這一步能消除團隊里95%的風格爭論。但clang-format管不了注釋寫法規(guī)范的注釋規(guī)則還是要靠代碼審查時提示。第三步是控制增量。對存量代碼不做大范圍重寫只要求新代碼和改動代碼符合規(guī)范。改到哪個文件就順手把這個文件的格式規(guī)范一遍。這樣漸進式推進既不影響業(yè)務(wù)節(jié)奏又能讓代碼庫的“規(guī)范覆蓋率”穩(wěn)步提升。最后一步是在團隊內(nèi)部建立共識編碼規(guī)范不是某些人拍腦袋定的教條而是從歷史代碼的血淚教訓里提煉出來的。規(guī)范里的每一條規(guī)則背后至少對應(yīng)過一次線上故障或者一次低效排查。這個共識建立起來之后團隊成員的執(zhí)行力會明顯提升因為他們是真心認可這些規(guī)則的價值。從那以后我每次給項目設(shè)計代碼結(jié)構(gòu)的時候都會強制走一遍規(guī)范的核心條目文件布局按照頭文件和實現(xiàn)文件的固定順序來排變量命名帶類型前綴長表達式斷行時把操作符放行首錯誤處理先于性能優(yōu)化考慮。一圈走下來代碼自己會說話。希望幫到你。本文還有配套的精品資源點擊獲取