范:八大原則與落地避坑指南)
簡介《中興通訊軟件編程規(guī)范——C/C》是中興通訊2003年發(fā)布的企業(yè)標準主要面向使用C/C的軟件工程師用于統(tǒng)一編碼風格、提升代碼質量。資源為1個doc文檔體積約486KB內容涵蓋基本原則、布局、注釋、命名規(guī)則、變量常量與類型、表達式與語句、函數與過程、可靠性、可測試性、斷言與錯誤處理等C/C開發(fā)的核心環(huán)節(jié)并在附錄A、B中給出了編程模版和規(guī)范檢查表方便在項目中落地執(zhí)行。規(guī)范按規(guī)則性和建議性分類既有必須嚴格執(zhí)行的規(guī)則也有可根據項目情況采納的建議并輔以大量正例、反例說明其“首先為人編寫程序其次才是計算機”的原則對提高代碼可讀性、可維護性和減少缺陷很有幫助。已有301人學習這份資料適合需要建立或完善編碼規(guī)范的研發(fā)團隊、企業(yè)內訓和自學的開發(fā)者。1. 先說結論一份2003年的 C/C 編碼規(guī)范最值錢的是那八個原則接手一個跑了七八年的通信設備驅動模塊讓人崩潰的往往不是算法本身而是代碼風格有些文件用 Tab有些用空格指針星號一會兒在類型后面一會兒貼在變量名上同一種錯誤碼在三個文件里有三種寫法。某通信設備廠商 2003 年發(fā)布的這份《軟件編程規(guī)范——C/C》就是為了治這種亂象。它把“為人編寫程序”“先正確性后效率”“避免全局變量”等 8 條原則放在最前后面用 12 個章節(jié)把布局、注釋、命名、可靠性、可測試性全部量化成可審核的規(guī)則。適合寫設備端、驅動、嵌入式協議?;蛘咭獛F隊收口代碼風格的開發(fā)者。想減少代碼評審吵架次數的人更值得從頭讀一遍。2. 規(guī)范總覽原則、規(guī)則、建議三層結構以及文件布局的硬性要求一份編碼規(guī)范能不能落地不看條目多少看它把約束分成幾個強度等級。這份規(guī)范開篇先定義術語再給基本原則然后才展開布局、注釋、命名等具體章節(jié)。所有條款被明確分成“原則”“規(guī)則”“建議”三層。原則管思想規(guī)則管行動建議留余地。后面十幾章的每一節(jié)基本都按“說明、正例、反例”的格式走讀起來不像法律條文更像一份帶示例的檢查單。2.1 三層約束原則管思想規(guī)則管行動建議留余地規(guī)范的總則部分一共 8 條原則很多人只把它當作口號實際上整份標準的邏輯都從這 8 條長出來。第 1 條“首先是為人編寫程序其次才是計算機”解釋了為什么要定這么多看起來瑣碎的布局規(guī)則。第 4 條“編程時首先達到正確性其次考慮效率”直接決定了后面可靠性和錯誤處理的優(yōu)先級。第 5 條“避免或少用全局變量”影響的是變量、常量與類型那一章的寫法。第 6 條“盡量避免使用 GOTO”在表達式與語句、函數與過程章節(jié)里都有呼應。從這里能看出這套規(guī)范的設計思路先統(tǒng)一價值觀再定操作規(guī)矩。如果把所有規(guī)則平鋪開發(fā)者容易陷入“為格式而格式”的誤區(qū)。先講清楚代碼是給人讀的后面的 80 列限制、注釋規(guī)范、命名規(guī)則才立得住。規(guī)則和建議的區(qū)分同樣重要。規(guī)則必須執(zhí)行比如不使用 Tab、{與}獨占一行、函數名后不留空格。建議則保留彈性比如某個 switch 分支是否一定要用{}包起來項目組可以根據實際情況決定。落地時不把規(guī)則和建議混為一談評審才不會被細節(jié)拖垮。2.2 頭文件布局先防重復包含再排四類聲明規(guī)范第二章“布局”首先規(guī)定的不是縮進而是文件布局順序。頭文件要按固定次序書寫文件頭注釋、#ifndef宏定義、其他條件編譯選項、#include然后才是常量定義、全局宏、全局數據類型、類定義和全局函數原型最后以#endif收尾。#ifndef DEVICE_MODULE_H #define DEVICE_MODULE_H #include stdio.h /* 標準庫頭文件用尖括號 */ #include dev_config.h /* 非標準庫頭文件用雙引號 */ #define MAX_BUF_LEN 128 /* 常量定義 */ typedef unsigned char BOOLEAN; /* 全局數據類型 */ class CDevice { public: CDevice(void); ~CDevice(void); int GetStatus(void); void SetStatus(int iStatus); private: int m_iStatus; }; #endif代碼里的#ifndef DEVICE_MODULE_H / #define DEVICE_MODULE_H / #endif就是防重復包含的標準做法。宏名通常取文件名大寫加下劃線避免和其他模塊撞名。#include的次序也有講究標準庫在前非標準庫在后這樣自定義頭文件里的依賴錯誤會更快暴露出來而不是被標準庫的隱藏包含掩蓋。在這個布局里常量、宏、數據類型、類的順序不能亂。先定義常量再定義依賴這些常量的宏接著定義類型最后定義類。把類的定義放在最靠后的位置保證類能引用前面所有聲明。我一般在新建頭文件時直接按這個模板填空比每次重新排布少犯錯。2.3 實現文件布局從包含到函數體的固定次序實現文件的布局同樣有明確順序文件頭注釋、#include、常量定義、文件內部使用的宏、內部數據類型、全局變量、靜態(tài)全局變量、局部函數原型、類的實現、全局函數、局部函數。#include device_module.h #include string.h #define INTERNAL_MARK 0xAA /* 文件內部使用的宏 */ static int s_iInternalTotal 0; /* 靜態(tài)全局變量 */ static int ValidateInput(int iInput); /* 局部函數原型 */ int DoProcess(int iInput) { int iRet ValidateInput(iInput); if (iRet ! 0) { return iRet; } s_iInternalTotal iInput; return iRet; } static int ValidateInput(int iInput) { if (iInput 0) { return -1; } return 0; }這種順序的本質是“先聲明后使用”。局部函數原型放在文件前面后面實現順序就可以任意排列不用再擔心編譯器報隱式聲明的錯。靜態(tài)全局變量集中聲明也方便維護者一眼看出哪些狀態(tài)被文件內部共享。實現文件還有一個容易被忽略的點類的實現放在全局函數之前。規(guī)范要求構造函數、析構函數、公有函數、保護函數、私有函數依次實現和類的聲明順序呼應。新人照著這個順序寫類代碼的可讀性會高很多。如果實現文件和頭文件聲明順序不一致查接口定義時要來回跳轉效率很低。3. 核心規(guī)則拆解布局、注釋、命名里那些一眼能查的硬指標布局章節(jié)是整份規(guī)范里最“密”的部分幾乎沒有廢話。從單行長度、縮進、空行到運算符兩側空格、斷行位置全部給了正例和反例。這些規(guī)則單獨看像強迫癥合在一起卻構成了一套完整的視覺系統(tǒng)代碼結構通過縮進和空行呈現邏輯邊界通過換行和括號表達。3.1 基礎格式和指針寫法80列、空行、星號位置規(guī)范要求一行代碼和注釋不超過 80 列包含空格。這條規(guī)則在今天的高分屏下看顯得保守但如果你需要對比老舊代碼、在終端里 diff80 列仍然有現實價值。另一個硬規(guī)則是一元操作符前后不加空格二元運算符和操作數之間至少留一個空格。if (iLen MAX_BUF_LEN) { iResult iLen 1; /* 二元運算符兩側有空格 */ }這里、、兩側都有空格關鍵字if和左括號之間留一個空格而函數名和左括號之間不留空格。視覺上關鍵字更突出函數調用更緊湊習慣了之后掃一眼就能區(qū)分if (...)和Function(...)。指針星號的位置是這個規(guī)范的一個特別之處要求*放在變量名前面而不是類型后面。float *pfBuffer; /* 正例星號靠近變量名 */ float* pfBuffer; /* 反例星號靠近類型規(guī)范明確反對 */站在 C 語言語法角度*確實是聲明符的一部分放在變量名前更準確。比如float *pfBuffer, *pfBuffer2;這種寫法能直接看出兩個變量都是指針。我見過很多團隊用float* pfBuffer風格也不算錯但如果整個存量工程都按規(guī)范走新代碼就必須跟隨老代碼的風格否則會有刺眼的混排。3.2 對齊與縮進4個空格、大括號獨占一行、等號排齊規(guī)范明確規(guī)定禁止使用 Tab必須用空格縮進縮進量為 4 個空格。{和}獨占一行并且和引用它們的語句左對齊。這一條和現代 clang-format 的默認風格不太一樣更像是 Allman 風格。if (iRet 0) { ProcessData(); } else { HandleError(); }大括號獨占一行的最大好處是括號配對非常直觀尤其在函數很長、嵌套很深時不會出現}和else粘在一起的情況。代價是代碼縱向變長一屏能看到的內容變少。這正是規(guī)范里“開始講究可讀性”的體現犧牲信息密度換取結構清晰。賦值語句的等號對齊屬于容易被忽視的細節(jié)。規(guī)范要求相關賦值語句的等號上下對齊我實際用下來發(fā)現在結構體初始化場景里特別有用tConfig.wHead 0; tConfig.wTail wMaxNum - 1; tConfig.wFree wMaxNum; tConfig.wAddress wConfigAddr; tConfig.wSize wConfigSize;對齊之后每一行賦值語句處理哪個字段、值是多少掃一眼就能看出來。如果不齊讀的時候需要對列逐個比對增加無意義的視覺負擔。類似的還有 switch 里的 case 分支建議用{}包起來避免變量跨 case 意外共享。switch (iCode) { case 1: { DoSomething(); break; } default: { DoNothing(); break; } }每個 case 用括號隔離能夠防止兩個分支共享同一個局部變量也減少了后面補代碼時不小心把變量聲明到 switch 外的情況。3.3 斷行規(guī)則長表達式從低優(yōu)先級操作符處拆超過 80 列的表達式必須斷行規(guī)范給出的原則是從低優(yōu)先級操作符處拆分操作符放新行之首。比如邏輯與、邏輯或||優(yōu)先級低于比較運算符就應該作為斷行的位置。if ((iFormat FORMAT_M) (iOfficeType TYPE_BSC_M)) { DoSomething(); }這里把放在第二行開頭閱讀時會先看到上一行條件再自然接上第二行條件邏輯流向更清楚。函數調用的斷行則在第一個參數處對齊iRet ReportStatusCheckPara(hWnd, ucCallNo, ucStatusReportNo);參數表逐行排開Ctrl 加光標可以快速上下移動查看每個參數。命名一個最容易踩的坑斷行時運算符留在上一行末尾很多 IDE 的自動折行會這樣處理但規(guī)范明確要求放在新行之首。這是因為行首運算符能提醒讀者“這行是上一行條件的延續(xù)”。3.4 注釋與命名寫意圖而不是復述代碼注釋部分的核心觀點是注釋要在功能、意圖層次上提供額外信息而不是重復代碼表面意思。/* 反例這行注釋完全多余 */ iCount; // iCount 加一 /* 正例解釋為什么這里需要加一 */ iCount; /* 補償上次分包丟棄的一幀數據 */判斷一條注釋是否值得寫可以看刪掉它之后是否損失信息。如果刪掉注釋代碼變得難懂說明注釋有價值如果代碼本身已經很清楚注釋就是在制造噪音。規(guī)范還要求注釋符和內容之間用一個空格分隔多行注釋統(tǒng)一用/* */單行注釋用//。命名規(guī)則這部分原文沒有給出完整清單但從正反例可以明顯看出它采用的是匈牙利前綴風格。指針變量用p開頭如pfBuffer整型用i如iLen、iCount無符號用u字符串用str結構體變量用t前綴如tConfig全局變量用g_靜態(tài)變量用s_成員變量用m_前綴。類名用C開頭如CDevice。這種命名在 2003 年的環(huán)境里很有價值因為當時的 IDE 補全能力弱看到名字就知道類型和變量作用域。今天的工程里是否繼續(xù)全量沿用取決于團隊代碼庫的年齡和工具鏈后面會專門說。4. 可靠性、可測試性與斷言把錯誤擋在上線之前這份規(guī)范的第 10 到第 12 章是整份文檔的精華可靠性、可測試性、斷言與錯誤處理。前面布局和命名規(guī)則決定代碼好不好讀這三章決定代碼能不能少出事故。老團隊愿意把“軟件編程規(guī)范”這份文檔當成硬標準很大程度是因為這些章節(jié)里的經驗都是從線上故障里反推出來的。4.1 可靠性優(yōu)先先正確后高效再談資源占用可靠性一章的基本思路在總則里已經埋下先正確性后健壯性再考慮效率和資源占用。落實到代碼上常見的處理方式是所有對外輸入先做合法性檢查所有可能失敗的操作都要有明確定義不能靜默失敗。int InitBuffer(Buffer *ptBuf, int iCapacity) { if (ptBuf NULL) { return -1; } if (iCapacity 0) { return -2; } ptBuf-pData malloc(iCapacity); if (ptBuf-pData NULL) { return -3; } ptBuf-iCapacity iCapacity; ptBuf-iLength 0; return 0; }這種逐層檢查的寫法看起來過度防御但是在通信設備這類長時間運行、資源受限的場景里任何一處空指針解引用都可能引起整機重啟。每個錯誤碼-1、-2、-3分別對應空指針、非法容量、內存分配失敗調用方可以根據返回值做不同處理。規(guī)范鼓勵這種顯式錯誤返回而不是用一個籠統(tǒng)的false掩蓋全部問題。另外需要強調的是規(guī)范第 5 章“變量、常量與類型”里還有一條相關要求局部變量必須初始化。常見的內存錯亂里很大比例來自未初始化的指針和數組下標。老代碼里經常見到聲明了一堆變量然后中間路徑直接返回變量沒有被賦值而后續(xù)代碼誤以為它已經被初始化了。4.2 斷言與錯誤處理不變量交給 assert外部異常交給返回值斷言和錯誤處理是兩種不同性質的機制很多新人在同一段代碼里混用。規(guī)范的思路是用斷言檢查程序內部不該發(fā)生的不變量用錯誤碼或返回值處理外部輸入和資源失敗。#include assert.h int SetBufferLength(Buffer *ptBuf, int iLen) { assert(ptBuf ! NULL); /* 內部不變量絕不應為空 */ assert(iLen 0 iLen ptBuf-iCapacity); if (ptBuf-pData NULL) { return -1; /* 外部條件需要返回錯誤 */ } ptBuf-iLength iLen; return 0; }這里assert(ptBuf ! NULL)斷言的是調用者不可能傳空指針給我如果傳了說明調用方代碼有 bug。斷言在發(fā)布版本里可能被NDEBUG編譯選項刪除所以它不能承擔錯誤處理職責。而pData NULL是運行時外部條件即使發(fā)生也能通過返回值讓上層恢復必須用錯誤處理機制兜底。我在看老項目的代碼時一個判斷代碼成熟度的標準就是內部接口用斷言外部接口用錯誤返回。如果反過來在內部函數里把所有不變量都返回錯誤碼發(fā)布版本雖然還在檢查但大量無意義的檢查會掩蓋真正的關鍵路徑。如果外部輸入被assert處理發(fā)布版本一旦上線直接放空指針進來程序就裸奔了。4.3 可測試性習慣函數邊界清晰副作用收斂可測試性章節(jié)的核心要求是最小化外部依賴和全局狀態(tài)。全局變量在這里不僅是耦合問題還是測試失敗時定位成本的問題。一個函數直接讀寫全局變量測試時想構造幾種不同輸入就得先想辦法重置全局狀態(tài)。/* 反例結果直接寫進全局變量 */ int g_iPacketCount 0; void ProcessPacket(const unsigned char *pucBuf, int iLen) { g_iPacketCount iLen; } /* 正例通過指針把結果交給調用方 */ int ProcessPacket(const unsigned char *pucBuf, int iLen, int *piOutCount) { if (pucBuf NULL || iLen 0) { return -1; } *piOutCount iLen; return 0; }正例把函數設計成“輸入參數 輸出參數 返回值”的標準結構不依賴任何文件級狀態(tài)。測試時只需要準備輸入和輸出緩沖斷言返回值和輸出即可。規(guī)范在函數與過程章節(jié)里也同樣強調單一職責、函數不宜過長這兩章合在一起基本就是現代單元測試的雛形。2003 年還沒有今天這么成熟的測試框架但這些思想放到現在完全不過時??蓽y試的代碼天然容易 review因為它把副作用擺在了明面上。5. 落地避坑指南2003年的規(guī)范用進今天工程的五個注意點好的規(guī)范如果不能落地就只是一份漂亮的 PDF。把這份 2003 年的標準引入現代團隊最大的障礙是工具鏈和代碼庫的現狀。下面這幾個坑我基本都踩過寫出來供參考。5.1 編輯器差異導致縮進翻車統(tǒng)一配置不是口號現象一個倉庫里有人用 Vim、有人用 VS Code、有人用 Qt Creator明明規(guī)范寫著“禁用 Tab、縮進 4 空格”最終提交的代碼仍然出現混排。原因是每個編輯器的默認 Tab 寬度不同有的轉成空格有的保留 Tab 字符。解決把規(guī)則寫進.editorconfig讓所有主流 IDE 自動讀取再在 Git 鉤子里攔截混合縮進。[*.{c,cpp,h}] indent_style space indent_size 4 end_of_line lf這個配置解決了“新建文件”的問題存量文件里的歷史縮進問題還得靠格式化工具分目錄處理。我一般會先對增量代碼強制檢查存量代碼只在改動到的函數內順手清理不搞一次性全倉格式化否則 diff 會大得沒法 review。5.2 匈牙利前綴和現代C風格打架現象規(guī)范要求成員變量帶m_前綴、指針帶p前綴但團隊引入現代 C 之后STL 容器、智能指針、模板代碼根本塞不進這套命名體系。結果出現m_iCount和readerIndex_混在一個文件里的情況風格分裂。原因不是規(guī)范的命名思路錯了而是它生前的語境是補全能力弱、類型信息藏在名字里的時代。今天的 IDE 能直接懸浮看到變量類型過度前綴反而讓名字變長。解決對老模塊保留既有匈牙利風格保持存量代碼一致性對新模塊只保留m_成員前綴去掉類型前綴。團隊內部明確這條邊界不搞一刀切。代碼評審時應重點關注命名是否達意而不是拘泥于前綴字母。5.3 把規(guī)則和建議混在一起評審評論區(qū)當場吵起來現象代碼 review 時A 說 switch 的 case 必須用括號包起來B 說規(guī)范里這只是建議。雙方都有依據但評審會開成了辯論賽。原因規(guī)范的普遍分級沒有被評審清單繼承大家對“建議”到底要不要執(zhí)行沒有共識。解決在評審清單里把規(guī)則分成“必須”和“建議”兩欄必須項納入 CI 檢查建議項只在有明確理由時才需要討論。我先按規(guī)范附錄 B 的檢查表過一遍把條目標記為強制的才放進提交門禁。這樣做之后評審討論減少了很多無意義的分歧。5.4 人工檢查容易漏工具掃描要跟上現象人工 review 布局規(guī)范時總會漏掉等號對齊、運算符空格這類細節(jié)因為人腦不擅長做高頻低難度檢查。原因規(guī)則條目分散在多個章節(jié)里單靠肉眼逐條對照效率很低而且容易疲倦。解決把布局規(guī)則和命名基礎規(guī)則轉成 clang-format 配置把空指針解引用、資源未釋放等可靠性風險交給 cppcheck 或 clang-tidy。規(guī)范是最好的輸入材料你只需要把規(guī)則逐條翻譯成工具參數剩下交給 CI 輸出違例文件列表。從附錄 B 的檢查表出發(fā)一張表基本就能映射出一套檢查腳本。5.5 老代碼一次性整改的包袱太大先做增量切換現象團隊想在舊項目里推行規(guī)范有人提議用格式工具全庫刷一遍結果生成上萬行 diff主干無法合并其他開發(fā)被沖突淹沒。原因存量代碼不符合規(guī)范是常態(tài)把格式規(guī)范當作整容手術一次做完必然產生巨大成本而且格式 diff 會掩蓋真正的邏輯改動review 形同虛設。解決采取“增量切換”策略。新代碼從第一天起強制規(guī)范舊代碼只在新功能改動時順手治理。啟動階段用 CI 對新增文件檢查格式和靜態(tài)分析不檢查歷史文件。經過一個版本周期的治理存量違規(guī)覆蓋率會明顯下降。我見過一個團隊用三個版本周期把十萬行存量代碼的違規(guī)率降到了 10% 以內過程中業(yè)務迭代沒有停過。6. 二次開發(fā)把企業(yè)標準剪裁成團隊自己的編碼規(guī)范拿來即用很誘人但真正有效的落地方式是先做剪裁再轉成工具配置最后用數據看效果。這套規(guī)范最大的資產是附錄 A 的編程模板和附錄 B 的規(guī)范檢查表二次開發(fā)的起點就是這張檢查表。第一步把檢查表逐條過一遍分類為“強制規(guī)則”“建議規(guī)則”“本團隊不適用的規(guī)則”。比如左括號換行的 Allman 風格如果團隊所有成員都寫 KR 風格很多年了不建議強行翻新把“大括號獨占一行”降級為“允許并保持完全一致”。這一步是在做減法不是做加法。第二步把強制規(guī)則映射到工具鏈上。布局和縮進由 clang-format 接管命名和可測性規(guī)則放進 review 清單。BasedOnStyle: LLVM IndentWidth: 4 UseTab: Never BreakBeforeBraces: Allman PointerAlignment: Right ColumnLimit: 80上面這份 clang-format 配置基本對應規(guī)范里的三條核心布局規(guī)則。這里有一個常見誤區(qū)生成配置之后要拿幾個真實源文件跑一遍看配置和規(guī)范的細節(jié)是否有沖突。例如 Allman 風格會讓else占一行如果你的團隊不習慣可以直接關掉這項不要為了“嚴格對標”把全員都逼瘋。第三步把檢查表放進 pre-commit 鉤子只檢查本次提交新增的文件。#!/bin/sh FILES$(git diff --cached --name-only --diff-filterACM -- *.c *.cpp *.h) if [ -n $FILES ]; then clang-format -i $FILES git add $FILES fi這個鉤子會在提交前自動格式化保證進入倉庫的代碼始終符合布局規(guī)則。不過要注意這里git add $FILES會把格式化后的文件重新納入暫存區(qū)提交前要確認沒有引入非預期的改動。最后一步是驗證。我通??慈齻€指標代碼評審里格式類評論占比、新代碼靜態(tài)檢查違規(guī)數、單次合并請求的文件改動量。這三個數字如果持續(xù)下降說明規(guī)范已經在起作用。從那以后我每次啟動新項目都會先做一次規(guī)范裁剪把強弱規(guī)則寫進 README再把檢查表轉成 CI 腳本最后才讓團隊開始寫第一行代碼。這套流程走完新增代碼基本不再需要討論格式問題評審討論集中在真正的邏輯和設計上。希望幫到你。本文還有配套的精品資源點擊獲取