C语言编程规范 clean code
目的
規則并不是完美的,通過禁止在特定情況下有用的特性,可能會對代碼實現造成影響。但是我們制定規則的目的“為了大多數程序員可以得到更多的好處”, 如果在團隊運作中認為某個規則無法遵循,希望可以共同改進該規則。參考該規范之前,希望您具有相應的C語言基礎能力,而不是通過該文檔來學習C語言。
了解C語言的ISO標準;
熟知C語言的基本語言特性;
了解C語言的標準庫;
總體原則
代碼需要在保證功能正確的前提下,滿足可讀、可維護、安全、可靠、可測試、高效、可移植的特征要求。
約定
規則:編程時必須遵守的約定
建議:編程時必須加以考慮的約定
無論是“規則”還是“建議”,都必須理解該條目這么規定的原因,并努力遵守。
例外
在不違背總體原則,經過充分考慮,有充足的理由的前提下,可以適當違背規范中約定。
例外破壞了代碼的一致性,請盡量避免?!耙巹t”的例外應該是極少的。
下列情況,應風格一致性原則優先:
修改外部開源代碼、第三方代碼時,應該遵守開源代碼、第三方代碼已有規范,保持風格統一。
1 命名
命名包括文件、函數、變量、類型、宏等命名。
命名被認為是軟件開發過程中最困難,也是最重要的事情。
標識符的命名要清晰、明了,有明確含義,符合閱讀習慣,容易理解。
統一的命名風格是一致性原則最直接的體現。
總體風格
駝峰風格(CamelCase)
大小寫字母混用,單詞連在一起,不同單詞間通過單詞首字母大寫來分開。
按連接后的首字母是否大寫,又分:?大駝峰(UpperCamelCase)和小駝峰(lowerCamelCase)
規則1.1 標識符命名使用駝峰風格
| 函數,結構體類型,枚舉類型,聯合體類型 | 大駝峰 |
| 變量,函數參數,宏參數,結構體中字段,聯合體中成員 | 小駝峰 |
| 宏,常量,枚舉值,goto 標簽 | 全大寫,下劃線分割 |
注意:
上表中常量是指,全局作用域下,const 修飾的基本數據類型、枚舉、字符串類型的變量,不包括數組、結構體和聯合體。
上表中變量是指除常量定義以外的其他變量,均使用小駝峰風格。
建議1.1 作用域越大,命名應越精確
C 與 C++ 不同,沒有名字空間,沒有類,所以全局作用域下的標識符命名要考慮不要沖突。
對于全局函數、全局變量、宏、類型名、枚舉名的命名,應當精確描述并全局唯一。
例:
int GetCount(void); // Bad: 描述不精確 int GetActiveConnectCount(void); // Good為了命名更精確,必要時可以增加模塊前綴。
模塊前綴與命名主體之間,按駝峰方式連接。
示例:
文件命名
建議1.2 文件命名統一采用小寫字符
文件名命名只允許使用小寫字母、數字以及下劃線(_)。
文件名應盡量簡短、準確、無二義性。
不大小寫混用的原因是,不同系統對文件名大小寫處理會不同(如 MicroSoft 的 DOS, Windows 系統不區分大小寫,但是 Unix / Linux, Mac 系統則默認區分)。
好的命名舉例:
dhcp_user_log.c
壞的命名舉例:
dhcp_user-log.c: 不推薦用'-'分隔
dhcpuserlog.c: 未分割單詞,可讀性差
函數命名
函數命名統一使用大駝峰風格。
建議1.3 函數的命名遵循閱讀習慣
動作類函數名,可以使用動賓結構。如:
AddTableEntry() // OK DeleteUser() // OK GetUserInfo() // OK判斷型函數,可以用形容詞,或加 is:
DataReady() // OK IsRunning() // OK JobDone() // OK數據型函數:
TotalCount() // OK GetTotalCount() // OK變量命名
變量命名使用小駝峰風格,包括全局變量,局部變量,函數聲明或定義中的參數,帶括號宏中的參數。
規則1.2 全局變量應增加 'g_' 前綴,函數內靜態變量命名不需要加特殊前綴
全局變量應當盡量少使用,使用時應特別注意,所以加上前綴用于視覺上的突出,促使開發人員對這些變量的使用更加小心。
全局靜態變量命名與全局變量相同,函數內的靜態變量命名與普通局部變量相同。
注意:常量本質也是全局變量,但如果命名風格是全大寫,下劃線連接的格式,則不適用當前規則。
建議1.4 局部變量應該簡短,且能夠表達相關含義
函數局部變量的命名,在能夠表達相關含義的前提下,應該簡短。
如下:
int Func(...) {enum PowerBoardStatus powerBoardStatusOfSlot; // Not good: 局部變量有點長powerBoardStatusOfSlot = GetPowerBoardStatus(slot);if (powerBoardStatusOfSlot == POWER_OFF) {...} ... }更好的寫法:
int Func(...) { enum PowerBoardStatus status; // Good: 結合上下文,status 已經能明確表達意思status = GetPowerBoardStatus(slot);if (status == POWER_OFF) {...}... }類似的, tmp 可以用來稱呼任意類型的臨時變量。
過短的變量命名應慎用,但有時候,單字符變量也是允許的,如用于循環語句中的計數器變量:
或一些簡單的數學計算函數中的變量:
int Mul(int a, int b) {return a * b; }類型命名
類型命名采用大駝峰命名風格。
類型包括結構體、聯合體、枚舉類型名。
例:
struct MsgHead {enum MsgType type;int msgLen;char *msgBuf; };union Packet {struct SendPacket send;struct RecvPacket recv; };enum BaseColor {RED, // 注意,枚舉類型是大駝峰,枚舉值應使用宏風格GREEN,BLUE };typedef int (*NodeCmpFunc)(struct Node *a, struct Node *b);通過 typedef 對結構體、聯合體、枚舉起別名時,盡量使用匿名類型。
若需要指針自嵌套,可以增加 'tag' 前綴或下劃線后綴。
宏、常量、枚舉命名
宏、枚舉值采用全大寫,下劃線連接的格式。
常量推薦采用全大寫,下劃線連接風格。作為全局變量,也可以保持與普通全局變量命名風格相同。
這里常量如前文定義,是指基本數據類型、枚舉、字符串類型的全局 const 變量。
函數式宏,如果功能上可以替代函數,也可以與函數的命名方式相同,使用大駝峰命名風格。
這種做法會讓宏與函數看起來一樣,容易混淆,需要特別注意。
宏舉例:
#define PI 3.14 #define MAX(a, b) (((a) < (b)) ? (b) : (a))#ifdef SOME_DEFINE void Bar(int); #define Foo(a) Bar(a) // 特殊場景,用大駝峰風格命名函數式宏 #else void Foo(int); #endif常量舉例:
const int VERSION = 200; // OK.const enum Color DEFAULT_COLOR = BLUE; // OK.const char PATH_SEP = '/'; // OK.const char * const GREETINGS = "Hello, World!"; // OK.非常量舉例:
// 結構體類型,不符合常量定義 const struct MyType g_myData = { ... }; // OK: 用小駝峰// 數組類型,不符合常量定義 const int g_xxxBaseValue[4] = { 1, 2, 4, 8 }; // OK: 用小駝峰int Foo(...) {// 局部作用域,不符合常量定義const int bufSize = 100; // OK: 用小駝峰... }枚舉舉例:
// 注意,枚舉類型名用大駝峰,其下面的取值是全大寫,下劃線相連 enum BaseColor {RED,GREEN,BLUE };建議1.5 避免函數式宏中的臨時變量命名污染外部作用域
首先,盡量少的使用函數式宏。
當函數式宏需要定義局部變量時,為了防止跟外部函數中的局部變量有命名沖突。
后置下劃線,是一種解決方案。例:
#define SWAP_INT(a, b) do { \int tmp_ = a; \a = b; \b = tmp_; \ } while (0)2 排版格式
行寬
建議2.1 行寬不超過 120 個字符
代碼行寬不宜過長,否則不利于閱讀。
控制行寬長度可以間接的引導開發去縮短函數、變量的命名,減少嵌套的層數,提升代碼可讀性。
強烈建議和要求每行字符數不要超過?120?個;除非超過?120?能顯著增加可讀性,并且不會隱藏信息。
雖然現代顯示器分辨率已經很高,但是行寬過長,反而提高了閱讀理解的難度;跟本規范提倡的“清晰”、“簡潔”原則相背。
如下場景不宜換行,可以例外:
換行會導致內容截斷,無法被方便查找(grep)的字符串,如命令行或 URL 等等。包含這些內容的代碼或注釋,可以適當例外。
#include / #error 語句可以超出行寬要求,但是也需要盡量避免。
例:
#ifndef XXX_YYY_ZZZ #error Header aaaa/bbbb/cccc/abc.h must only be included after xxxx/yyyy/zzzz/xyz.h #endif縮進
規則2.1 使用空格進行縮進,每次縮進4個空格
只允許使用空格(space)進行縮進,每次縮進為?4?個空格。不允許使用Tab鍵進行縮進。
當前幾乎所有的集成開發環境(IDE)和代碼編輯器都支持配置將Tab鍵自動擴展為4空格輸入,請配置你的代碼編輯器支持使用空格進行縮進。
大括號
規則2.2 使用 K&R 縮進風格
K&R風格
換行時,函數左大括號另起一行放行首,并獨占一行;其他左大括號跟隨語句放行末。
右大括號獨占一行,除非后面跟著同一語句的剩余部分,如 do 語句中的 while,或者 if 語句的 else/else if,或者逗號、分號。
如:
struct MyType { // Good: 跟隨語句放行末,前置1空格... }; // Good: 右大括號后面緊跟分號int Foo(int a) { // Good: 函數左大括號獨占一行,放行首if (...) {...} else { // Good: 右大括號與 else 語句在同一行...} // Good: 右大括號獨占一行 }函數聲明和定義
規則2.3 函數聲明、定義的返回類型和函數名在同一行;函數參數列表換行時應合理對齊
在聲明和定義函數的時候,函數的返回值類型應該和函數名在同一行。
函數參數列表換行時,應合理對齊。
參數列表的左圓括號總是和函數名在同一行,不要單獨一行;右圓括號總是跟隨最后一個參數。
換行舉例:
ReturnType FunctionName(ArgType paramName1, ArgType paramName2) // Good:全在同一行 {... }ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1, // 行寬不滿足所有參數,進行換行ArgType paramName2, // Good:和上一行參數對齊ArgType paramName3) {... }ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // 行寬限制,進行換行ArgType paramName3, ArgType paramName4, ArgType paramName5) // Good: 換行后 4 空格縮進 {... }ReturnType ReallyReallyReallyReallyLongFunctionName( // 行寬不滿足第1個參數,直接換行ArgType paramName1, ArgType paramName2, ArgType paramName3) // Good: 換行后 4 空格縮進 {... }函數調用
規則2.4 函數調用參數列表換行時保持參數進行合理對齊
函數調用時,函數參數列表如果換行,應該進行合理的參數對齊。
左圓括號總是跟函數名,右圓括號總是跟最后一個參數。
換行舉例:
ReturnType result = FunctionName(paramName1, paramName2); // Good:函數參數放在一行ReturnType result = FunctionName(paramName1,paramName2, // Good:保持與上方參數對齊paramName3);ReturnType result = FunctionName(paramName1, paramName2, paramName3, paramName4, paramName5); // Good:參數換行,4 空格縮進ReturnType result = VeryVeryVeryLongFunctionName( // 行寬不滿足第1個參數,直接換行paramName1, paramName2, paramName3); // 換行后,4 空格縮進如果函數調用的參數存在內在關聯性,按照可理解性優先于格式排版要求,對參數進行合理分組換行。
// Good:每行的參數代表一組相關性較強的數據結構,放在一行便于理解 int result = DealWithStructureLikeParams(left.x, left.y, // 表示一組相關參數right.x, right.y); // 表示另外一組相關參數條件語句
規則2.5 條件語句必須要使用大括號
我們要求條件語句都需要使用大括號,即便只有一條語句。
理由:
代碼邏輯直觀,易讀;
在已有條件語句代碼上增加新代碼時不容易出錯;
對于在條件語句中使用函數式宏時,沒有大括號保護容易出錯(如果宏定義時遺漏了大括號)。
規則2.6 禁止 if/else/else if 寫在同一行
條件語句中,若有多個分支,應該寫在不同行。
如下是正確的寫法:
if (someConditions) {... } else { // Good: else 與 if 在不同行... }下面是不符合規范的案例:
if (someConditions) { ... } else { ... } // Bad: else 與 if 在同一行循環
規則2.7 循環語句必須使用大括號
和條件表達式類似,我們要求for/while循環語句必須加上大括號,即便循環體是空的,或循環語句只有一條。
for (int i = 0; i < someRange; i++) { // Good: 使用了大括號DoSomething(); }while (condition) { } // Good:循環體是空,使用大括號while (condition) { continue; // Good:continue 表示空邏輯,使用大括號 }壞的例子:
for (int i = 0; i < someRange; i++)DoSomething(); // Bad: 應該加上括號while (condition); // Bad:使用分號容易讓人誤解是while語句中的一部分switch語句
規則2.8 switch 語句的 case/default 要縮進一層
switch 語句的縮進風格如下:
switch (var) {case 0: // Good: 縮進DoSomething1(); // Good: 縮進break;case 1: { // Good: 帶大括號格式DoSomething2();break;}default:break; }switch (var) { case 0: // Bad: case 未縮進DoSomething();break; default: // Bad: default 未縮進break; }表達式
建議2.2 表達式換行要保持換行的一致性,操作符放行末
較長的表達式,不滿足行寬要求的時候,需要在適當的地方換行。一般在較低優先級操作符或連接符后面截斷,操作符或連接符放在行末。
操作符、連接符放在行末,表示“未結束,后續還有”。
例:
// 假設下面第一行已經不滿足行寬要求 if ((currentValue > MIN) && // Good:換行后,布爾操作符放在行末(currentValue < MAX)) { DoSomething();... }int result = reallyReallyLongVariableName1 + // Good: 加號留在行末reallyReallyLongVariableName2;表達式換行后,注意保持合理對齊,或者4空格縮進。參考下面例子
int sum = longVaribleName1 + longVaribleName2 + longVaribleName3 +longVaribleName4 + longVaribleName5 + longVaribleName6; // OK: 4空格縮進int sum = longVaribleName1 + longVaribleName2 + longVaribleName3 +longVaribleName4 + longVaribleName5 + longVaribleName6; // OK: 保持對齊變量賦值
規則2.9 多個變量定義和賦值語句不允許寫在一行
每行最好只有一個變量初始化的語句,更容易閱讀和理解。
int maxCount = 10; bool isCompleted = false;下面是不符合規范的示例:
int maxCount = 10; bool isCompleted = false; // Bad:多個初始化放在了同一行 int x, y = 0; // Bad:多個變量定義需要分行,每行一個int pointX; int pointY; ... pointX = 1; pointY = 2; // Bad:多個變量賦值語句放同一行例外情況:
對于多個相關性強的變量定義,且無需初始化時,可以定義在一行,減少重復信息,以便代碼更加緊湊。
初始化
初始化包括結構體、聯合體及數組的初始化
規則2.10 初始化換行時要有縮進,或進行合理對齊
結構體或數組初始化時,如果換行應保持4空格縮進。
從可讀性角度出發,選擇換行點和對齊位置。
對于復雜結構數據的初始化,盡量清晰、緊湊。
參考如下格式:
注意:
左大括號放行末時,對應的右大括號需另起一行
左大括號被內容跟隨時,對應的右大括號也應跟隨內容
規則2.11 結構體和聯合體在按成員初始化時,每個成員初始化單獨一行
C99標準支持結構體和聯合體按照成員進行初始化,標準中叫"指定初始化"(designated initializer)。如果按照這種方式進行初始化,每個成員的初始化單獨一行。
struct Date {int year;int month;int day; };struct Date date = { // Good:使用指定初始化方式時,每行初始化一個.year = 2000,.month = 1,.day = 1 };指針
建議2.3 指針類型"*"跟隨變量名或者類型,不要兩邊都留有空格或都沒有空格
聲明或定義指針變量或者返回指針類型函數時,"*" 靠左靠右都可以,但是不要兩邊都有或者都沒有空格。
int *p1; // OK. int* p2; // OK.int*p3; // Bad: 兩邊都沒空格 int * p4; // Bad: 兩邊都有空格選擇一種風格,并保持一致性。
選擇"*"跟隨類型風格時,避免一行同時聲明帶指針的多個變量。
int* a, b; // Bad: 很容易將 b 誤理解成指針選擇"*"跟隨變量風格時,可能會存在無法緊跟的情況。
無法跟隨時就不跟隨,不要破壞風格一致性。
注意,任何時候 "*" 不要緊跟 const 或 restrict 關鍵字。
編譯預處理
規則2.12 編譯預處理的"#"默認放在行首,嵌套編譯預處理語句時,"#"可以進行縮進
編譯預處理的"#"統一放在行首;即便編譯預處理的代碼是嵌入在函數體中的,"#"也應該放在行首。
空格和空行
規則2.13 水平空格應該突出關鍵字和重要信息,避免不必要的留白
水平空格應該突出關鍵字和重要信息,每行代碼尾部不要加空格??傮w規則如下:
if, switch, case, do, while, for 等關鍵字之后加空格;
小括號內部的兩側,不要加空格
二元操作符(= + ‐ < > * / % | & ^ <= >= == !=)左右兩側加空格
一元操作符(& * + ‐ ~ !)之后不要加空格
三目操作符(? :)符號兩側均需要空格
結構體中表示位域的冒號,兩側均需要空格
前置和后置的自增、自減(++ --)和變量之間不加空格
結構體成員操作符(. ->)前后不加空格
大括號內部兩側有無空格,左右必須保持一致
逗號、分號、冒號(不含三目操作符和表示位域的冒號)緊跟前面內容無空格,其后需要空格
函數參數列表的小括號與函數名之間無空格
類型強制轉換的小括號與被轉換對象之間無空格
數組的中括號與數組名之間無空格
涉及到換行時,行末的空格可以省去
對于大括號內部兩側的空格,建議如下:
一般的,大括號內部兩側建議加空格
對于空的,或單個標識符,或單個字面常量,空格不是必須 如:'{}', '{0}', '{NULL}', '{"hi"}' 等
連續嵌套的多重括號之間,空格不是必須 如:'{{0}}', '{{ 1, 2 }}' 等 錯誤示例:'{ 0, {1}}',不屬于連續嵌套場景,而且最外側大括號左右不一致
常規情況:
int i = 0; // Good:變量初始化時,= 前后應該有空格,分號前面不要留空格 int buf[BUF_SIZE] = {0}; // Good:數組初始化時,大括號內空格可選 int arr[] = { 10, 20 }; // Good: 正常大括號內部兩側建議加空格函數定義和函數調用:
int result = Foo(arg1,arg2); ^ // Bad: 逗號后面應該有空格int result = Foo( arg1, arg2 );^ ^ // Bad: 小括號內部兩側不應該有空格指針和取地址
x = *p; // Good:*操作符和指針p之間不加空格 p = &x; // Good:&操作符和變量x之間不加空格 x = r.y; // Good:通過.訪問成員變量時不加空格 x = r->y; // Good:通過->訪問成員變量時不加空格操作符:
x = 0; // Good:賦值操作的=前后都要加空格 x = -5; // Good:負數的符號和數值之前不要加空格 ++x; // Good:前置和后置的++/--和變量之間不要加空格 x--;if (x && !y) // Good:布爾操作符前后要加上空格,!操作和變量之間不要空格 v = w * x + y / z; // Good:二元操作符前后要加空格 v = w * (x + z); // Good:括號內的表達式前后不需要加空格循環和條件語句:
if (condition) { // Good:if關鍵字和括號之間加空格,括號內條件語句前后不加空格... } else { // Good:else關鍵字和大括號之間加空格... }while (condition) {} // Good:while關鍵字和括號之間加空格,括號內條件語句前后不加空格for (int i = 0; i < someRange; ++i) { // Good:for關鍵字和括號之間加空格,分號之后加空格... }switch (var) { // Good: switch 關鍵字后面有1空格case 0: // Good:case語句條件和冒號之間不加空格...break;...default:...break; }注意:當前的集成開發環境(IDE)和代碼編輯器都可以設置刪除行尾的空格,請正確配置你的編輯器。
建議2.4 合理安排空行,保持代碼緊湊
減少不必要的空行,可以顯示更多的代碼,方便代碼閱讀。下面有一些建議遵守的規則:
根據上下內容的相關程度,合理安排空行;
函數內部、類型定義內部、宏內部、初始化表達式內部,不使用連續空行
不使用連續?3?個空行,或更多
大括號內的代碼塊首行之前和末行之后不要加空行。
3 注釋
一般的,盡量通過清晰的架構邏輯,好的符號命名來提高代碼可讀性;需要的時候,才輔以注釋說明。
注釋是為了幫助閱讀者快速讀懂代碼,所以要從讀者的角度出發,按需注釋。
注釋內容要簡潔、明了、無二義性,信息全面且不冗余。
注釋跟代碼一樣重要。
寫注釋時要換位思考,用注釋去表達此時讀者真正需要的信息。在代碼的功能、意圖層次上進行注釋,即注釋解釋代碼難以表達的意圖,不要重復代碼信息。
修改代碼時,也要保證其相關注釋的一致性。只改代碼,不改注釋是一種不文明行為,破壞了代碼與注釋的一致性,讓閱讀者迷惑、費解,甚至誤解。
使用英文進行注釋。
注釋風格
在 C 代碼中,使用?/*?*/和?//?都是可以的。
按注釋的目的和位置,注釋可分為不同的類型,如文件頭注釋、函數頭注釋、代碼注釋等等;
同一類型的注釋應該保持統一的風格。
注意:本文示例代碼中,大量使用 '//' 后置注釋只是為了更精確的描述問題,并不代表這種注釋風格更好。
文件頭注釋
規則3.1 文件頭注釋必須包含版權許可
/*
Copyright (c) 2020 XXX
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */
函數頭注釋
規則3.2 禁止空有格式的函數頭注釋
并不是所有的函數都需要函數頭注釋;
函數原型無法表達的信息,加函數頭注釋輔助說明;
函數頭注釋統一放在函數聲明或定義上方。
選擇使用如下風格之一:
使用'//'寫函數頭
使用'/*' '*/' 寫函數頭
/* 單行函數頭 */ int Func1(void);/** 單行或多行函數頭* 第二行*/ int Func2(void);函數盡量通過函數名自注釋,按需寫函數頭注釋。
不要寫無用、信息冗余的函數頭;不要寫空有格式的函數頭。
函數頭注釋內容可選,但不限于:功能說明、返回值,性能約束、用法、內存約定、算法實現、可重入的要求等等。
模塊對外頭文件中的函數接口聲明,其函數頭注釋,應當將重要、有用的信息表達清楚。
例:
/** 返回實際寫入的字節數,-1表示寫入失敗* 注意,內存 buf 由調用者負責釋放*/ int WriteString(char *buf, int len);壞的例子:
/** 函數名:WriteString* 功能:寫入字符串* 參數:* 返回值:*/ int WriteString(char *buf, int len);上面例子中的問題:
參數、返回值,空有格式沒內容
函數名信息冗余
關鍵的 buf 由誰釋放沒有說清楚
代碼注釋
規則3.3 代碼注釋放于對應代碼的上方或右邊
規則3.4 注釋符與注釋內容間要有1空格;右置注釋與前面代碼至少1空格
代碼上方的注釋,應該保持對應代碼一樣的縮進。
選擇并統一使用如下風格之一:
使用'//'
使用'/*' '*/'
/* 這是單行注釋 */ DoSomething();/** 這是單/多行注釋* 第二行*/ DoSomething();代碼右邊的注釋,與代碼之間,至少留1空格,建議不超過4空格。
通常使用擴展后的 TAB 鍵即可實現 1-4 空格的縮進。
選擇并統一使用如下風格之一:
int foo = 100; // 放右邊的注釋 int bar = 200; /* 放右邊的注釋 */右置格式在適當的時候,上下對齊會更美觀。
對齊后的注釋,離左邊代碼最近的那一行,保證1-4空格的間隔。
例:
當右置的注釋超過行寬時,請考慮將注釋置于代碼上方。
規則3.5 不用的代碼段直接刪除,不要注釋掉
被注釋掉的代碼,無法被正常維護;當企圖恢復使用這段代碼時,極有可能引入易被忽略的缺陷。
正確的做法是,不需要的代碼直接刪除掉。若再需要時,考慮移植或重寫這段代碼。
這里說的注釋掉代碼,包括用 /* */ 和 //,還包括 #if 0, #ifdef NEVER_DEFINED 等等。
建議3.1 case語句塊結束時如果不加break/return,需要有注釋說明(fall-through)
有時候需要對多個case標簽做相同的事情,case語句在結束不加break或return,直接執行下一個case標簽中的語句,這在C語法中稱之為"fall-through"。
這種情況下,需要在"fall-through"的地方加上注釋,清晰明確的表達出這樣做的意圖;或者至少顯式指明是 "fall-through"。
例,顯式指明 fall-through:
switch (var) {case 0:DoSomething();/* fall-through */case 1:DoSomeOtherThing();...break;default: DoNothing();break; }如果 case 語句是空語句,則可以不用加注釋特別說明:
switch (var) {case 0:case 1:DoSomething();break;default:DoNothing();break; }4 頭文件
對于C語言來說,頭文件的設計體現了大部分的系統設計。
正確使用頭文件可使代碼在可讀性、文件大小和編譯構建性能上大為改觀。
本章從編程規范的角度總結了一些方法,可用于幫助合理規劃頭文件。
頭文件職責
頭文件是模塊或文件的對外接口。
頭文件中適合放置接口的聲明,不適合放置實現(內聯函數除外)。
頭文件應當職責單一。頭文件過于復雜,依賴過于復雜還是導致編譯時間過長的主要原因。
建議4.1 每一個.c文件都應該有相應的.h文件,用于聲明需要對外公開的接口
通常情況下,每個.c文件都有一個相應的.h(并不一定同名),用于放置對外提供的函數聲明、宏定義、類型定義等。
如果一個.c文件不需要對外公布任何接口,則其就不應當存在。
例外:程序的入口(如main函數所在的文件),單元測試代碼,動態庫代碼。
示例:
foo.h 內容
foo.c 內容
static void Bar(void); // Good: 對內函數的聲明放在.c文件的頭部,并聲明為static限制其作用域void Foo(void) {Bar(); }static void Bar(void) {// Do something; }內部使用的函數聲明,宏、枚舉、結構體等定義不應放在頭文件中。
有些產品中,習慣一個.c文件對應兩個.h文件,一個用于存放對外公開的接口,一個用于存放內部需要用到的定義、聲明等,以控制.c文件的代碼行數。
不提倡這種風格,產生這種風格的根源在于.c過大,應當首先考慮拆分.c文件。
另外,一旦把私有定義、聲明放到獨立的頭文件中,就無法從技術上避免別人包含。
本規則反過來并不一定成立。比如:
有些特別簡單的頭文件,如命令 ID 定義頭文件,不需要有對應的.c存在。
同一套接口協議下,有多個實例,由于接口相同且穩定,所以允許出現一個.h對應多個.c文件。
建議4.2 頭文件的擴展名只使用.h,不使用非習慣用法的擴展名,如.inc
有些產品中使用了 .inc 作為頭文件擴展名,這不符合C語言的習慣用法。在使用 .inc 作為頭文件擴展名的產品,習慣上用于標識此頭文件為私有頭文件。但是從產品的實際代碼來看,這一條并沒有被遵守,一個 .inc 文件被多個 .c 包含。本規范不提倡將私有定義單獨放在頭文件中,具體見建議4.1。
頭文件依賴
頭文件包含是一種依賴關系,頭文件應向穩定的方向包含。
一般來說,應當讓不穩定的模塊依賴穩定的模塊,從而當不穩定的模塊發生變化時,不會影響(編譯)穩定的模塊。
依賴的方向應該是:產品依賴于平臺,平臺依賴于標準庫。
除了不穩定的模塊依賴于穩定的模塊外,更好的方式是每個模塊都依賴于接口,這樣任何一個模塊的內部實現更改都不需要重新編譯另外一個模塊。
在這里,假設接口本身是最穩定的。
規則4.1 禁止頭文件循環依賴
頭文件循環依賴,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 導致任何一個頭文件修改,都導致所有包含了a.h/b.h/c.h的代碼全部重新編譯一遍。
而如果是單向依賴,如a.h包含b.h,b.h包含c.h,而c.h不包含任何頭文件,則修改a.h不會導致包含了b.h/c.h的源代碼重新編譯。
頭文件循環依賴直接體現了架構設計上的不合理,可通過架構優化來避免。
規則4.2 頭文件必須編寫#define保護,防止重復包含
為防止頭文件被多重包含,所有頭文件都應當使用 #define 作為包含保護;不要使用 #pragma once
定義包含保護符時,應該遵守如下規則:
保護符使用唯一名稱;建議考慮項目源代碼樹頂層以下的文件路徑
不要在受保護部分的前后放置代碼或者注釋,文件頭注釋除外。
假定 timer 模塊的 timer.h,其目錄為?timer/include/timer.h。其保護符若使用 'TIME_H' 很容易不唯一,所以使用項目源代碼樹的全路徑,如:
#ifndef TIMER_INCLUDE_TIMER_H #define TIMER_INCLUDE_TIMER_H...#endif規則4.3 禁止通過聲明的方式引用外部函數接口、變量
只能通過包含頭文件的方式使用其他模塊或文件提供的接口。
通過 extern 聲明的方式使用外部函數接口、變量,容易在外部接口改變時可能導致聲明和定義不一致。
同時這種隱式依賴,容易導致架構腐化。
不符合規范的案例:
a.c 內容
應該改為:
a.c 內容
b.h 內容
int Foo(void);b.c內容
int Foo(void) {// Do something }例外,有些場景需要引用其內部函數,但并不想侵入代碼時,可以 extern 聲明方式引用。
如:
針對某一內部函數進行單元測試時,可以通過 extern 聲明來引用被測函數;
當需要對某一函數進行打樁、打補丁處理時,允許 extern 聲明該函數。
規則4.4 禁止在 extern "C" 中包含頭文件
在 extern "C" 中包含頭文件,有可能會導致 extern "C" 嵌套,部分編譯器對 extern "C" 嵌套層次有限制,嵌套層次太多會編譯錯誤。
extern "C" 通常出現在 C,C++ 混合編程的情況下,在 extern "C" 中包含頭文件,可能會導致被包含頭文件的原有意圖遭到破壞,比如鏈接規范被不正確地更改。
示例,存在a.h和b.h兩個頭文件:
a.h 內容
b.h 內容
... #ifdef __cplusplus extern "C" { #endif#include "a.h" void B(void);#ifdef __cplusplus } #endif使用C++預處理器展開b.h,將會得到
extern "C" {void Foo(int);void B(void); }按照 a.h 作者的本意,函數 Foo 是一個 C++ 自由函數,其鏈接規范為 "C++"。但在 b.h 中,由于?#include "a.h"?被放到了?extern "C"?的內部,函數 Foo 的鏈接規范被不正確地更改了。
例外:如果在 C++ 編譯環境中,想引用純C的頭文件,這些C頭文件并沒有?extern "C"?修飾。非侵入式的做法是,在?extern "C"?中去包含C頭文件。
5 函數
函數的作用:避免重復代碼、增加可重用性;分層,降低復雜度、隱藏實現細節,使程序更加模塊化,從而更有利于程序的閱讀,維護。
函數應該簡潔、短小。
一個函數只完成一件事情。
函數設計
函數設計的精髓:編寫整潔函數,同時把代碼有效組織起來。代碼簡單直接、不隱藏設計者的意圖、用干凈利落的抽象和直截了當的控制語句將函數有機組織起來。
規則5.1 避免函數過長,函數不超過50行(非空非注釋)
函數應該可以一屏顯示完 (50行以內),只做一件事情,而且把它做好。
過長的函數往往意味著函數功能不單一,過于復雜,或過分呈現細節,未進行進一步抽象。
例外:
考慮代碼的聚合性與功能的全面性,某些函數可能會超過50行,但前提是不影響代碼的可讀性與簡潔。
這些例外的函數應該是極少的,例如特定算法處理。
即使一個長函數現在工作的非常好, 一旦有人對其修改, 有可能出現新的問題, 甚至導致難以發現的bug。
建議將其拆分為更加簡短并易于管理的若干函數,以便于他人閱讀和修改代碼。
規則5.2 避免函數的代碼塊嵌套過深,不要超過4層
函數的代碼塊嵌套深度指的是函數中的代碼控制塊(例如:if、for、while、switch等)之間互相包含的深度。
每級嵌套都會增加閱讀代碼時的腦力消耗,因為需要在腦子里維護一個“?!?#xff08;比如,進入條件語句、進入循環等等)。
應該做進一步的功能分解,從而避免使代碼的閱讀者一次記住太多的上下文。
使用衛語句可以有效的減少 if 相關的嵌套層次。例:
原代碼嵌套層數是 3:
使用衛語句重構,嵌套層數變成 2:
int Foo(...) {if (!received) { // Good: 使用'衛語句'return -1;}type = GetMsgType(msg);if (type == UNKNOWN) {return -1;}return DealMsg(..); }例外:
考慮代碼的聚合性與功能的全面性,某些函數嵌套可能會超過4層,但前提是不影響代碼的可讀性與簡潔。
這些例外的函數應該是極少的。
建議5.1 對函數的錯誤返回碼要全面處理
一個函數(標準庫中的函數/第三方庫函數/用戶定義的函數)能夠提供一些指示錯誤發生的方法。這可以通過使用錯誤標記、特殊的返回數據或者其他手段,不管什么時候函數提供了這樣的機制,調用程序應該在函數返回時立刻檢查錯誤指示。
示例:
char fileHead[128]; ReadFileHead(fileName, fileHead, sizeof(fileHead)); // Bad: 未檢查返回值DealWithFileHead(fileHead, sizeof(fileHead)); // fileHead 可能無效正確寫法:
char fileHead[128]; ret = ReadFileHead(fileName, fileHead, sizeof(fileHead)); if (ret != OK) { // Good: 確保 fileHead 被有效寫入return ERROR; }DealWithFileHead(fileHead, sizeof(fileHead)); // 處理文件頭注意,當函數返回值被大量的顯式(void)忽略掉時,應當考慮函數返回值的設計是否合理。
如果所有調用者都不關注函數返回值時,請將函數設計成void型。
函數參數
建議5.2 設計函數時,優先使用返回值而不是輸出參數
使用返回值而不是輸出參數,可以提高可讀性,并且通常提供相同或更好的性能。
函數名為 GetXxx、FindXxx 或直接名詞作函數名的函數,直接返回對應對象,可讀性更好。
建議5.3 使用強類型參數,避免使用void*
盡管不同的語言對待強類型和弱類型有自己的觀點,但是一般認為c/c++是強類型語言,既然我們使用的語言是強類型的,就應該保持這樣的風格。
好處是盡量讓編譯器在編譯階段就檢查出類型不匹配的問題。
使用強類型便于編譯器幫我們發現錯誤,如下代碼中注意函數?FooListAddNode?的使用:
struct FooNode {struct List link;int foo; };struct BarNode {struct List link;int bar; }void FooListAddNode(void *node) // Bad: 這里用 void * 類型傳遞參數 {FooNode *foo = (FooNode *)node;ListAppend(&g_fooList, &foo->link); }void MakeTheList(...) {FooNode *foo;BarNode *bar;...FooListAddNode(bar); // Wrong: 這里本意是想傳遞參數 foo,但錯傳了 bar,卻沒有報錯 }上述問題有可能很隱晦,不易輕易暴露,從而破壞性更大。
如果明確?FooListAddNode?的參數類型,而不是?void *,則在編譯階段就能發現上述問題。
例外:某些通用泛型接口,需要傳入不同類型指針的,可以用?void *?入參。
建議5.4 模塊內部函數參數的合法性檢查,由調用者負責
對于模塊外部傳入的參數,必須進行合法性檢查,保護程序免遭非法輸入數據的破壞。
模塊內部函數調用,缺省由調用者負責保證參數的合法性,如果都由被調用者來檢查參數合法性,可能會出現同一個參數,被檢查多次,產生冗余代碼,很不簡潔。
由調用者保證入參的合法性,這種契約式編程能讓代碼邏輯更簡潔,可讀性更好。
示例:
建議5.5 函數的指針參數如果不是用于修改所指向的對象就應該聲明為指向const的指針
const 指針參數,將限制函數通過該指針修改所指向對象,使代碼更牢固、安全。
示例:C99標準 7.21.4.4 中strncmp 的例子,不變參數聲明為const。
int strncmp(const char *s1, const char *s2, size_t n); // Good:不變參數聲明為const注意:指針參數要不要加 const 取決于函數設計,而不是看函數實體內有沒有發生“修改對象”的動作。
建議5.6 函數的參數個數不超過5個
函數的參數過多,會使得該函數易于受外部(其他部分的代碼)變化的影響,從而影響維護工作。函數的參數過多同時也會增大測試的工作量。
函數的參數個數不要超過5個,如果超過可以考慮:
看能否拆分函數
看能否將相關參數合在一起,定義結構體
內聯函數
內聯函數是C99引入的一種函數優化手段。函數內聯能消除函數調用的開銷;并得益于內聯實現跟調用點代碼的合并,編譯器有更大的視角,從而完成更多的代碼優化。內聯函數跟函數式宏比較類似,兩者的分析詳見建議6.1。
建議5.7 內聯函數不超過10行(非空非注釋)
將函數定義成內聯一般希望提升性能,但是實際并不一定能提升性能。如果函數體短小,則函數內聯可以有效的縮減目標代碼的大小,并提升函數執行效率。
反之,函數體比較大,內聯展開會導致目標代碼的膨脹,特別是當調用點很多時,膨脹得更厲害,反而會降低執行效率。
內聯函數規模建議控制在?10?行以內。
不要為了提高性能而濫用內聯函數。不要過早優化。一般情況,當有實際測試數據證明內聯性能更高時,再將函數定義為內聯。對于類似 setter/getter 短小而且調用頻繁的函數,可以定義為內聯。
規則5.3 被多個源文件調用的內聯函數要放在頭文件中定義
內聯函數是在編譯時內聯展開,因此要求內聯函數定義必須在調用此函數的每個源文件內可見。
如下所示代碼,inline.h 只有SomeInlineFunc函數的聲明而沒有定義。other.c包含inline.h,調用SomeInlineFunc時無法內聯。
inline.h
inline int SomeInlineFunc(void);inline.c
inline int SomeInlineFunc(void) {// 實現代碼 }other.c
#include "inline.h" int OtherFunc(void) {int ret = SomeInlineFunc(); }由于這個限制,多個源文件如果要調用同一個內聯函數,需要將內聯函數的定義放在頭文件中。
gnu89?在內聯函數實現上跟C99標準有差異,兼容做法是將函數聲明為?static inline。
6 宏
函數式宏(function-like macro)
函數式宏是指形如函數的宏(示例代碼如下所示),其包含若干條語句來實現某一特定功能。
#define ASSERT(x) do { \if (!(x)) { \printk(KERN_EMERG "assertion failed %s: %d: %s\n", \__FILE__, __LINE__, #x); \BUG(); \} \ } while (0)建議6.1 使用函數代替函數式宏
定義函數式宏前,應考慮能否用函數替代。對于可替代場景,建議用函數替代宏。
函數式宏的缺點如下:
函數式宏缺乏類型檢查,不如函數調用檢查嚴格。示例代碼見下。
宏展開時宏參數不求值,可能會產生非預期結果,詳見規則6.1和規則6.3。
宏沒有獨立的作用域,跟控制流語句配合時,可能會產生如規則6.2描述的非預期結果。
宏的技巧性太強(參見下面的規則),例如#的用法和無處不在的括號,影響可讀性。
在特定場景下必須用特定編譯器對宏的擴展,如?gcc?的?statement expression,可移植性也不好。
宏在預編譯階段展開后,在其后編譯、鏈接和調試時都不可見;而且包含多行的宏會展開為一行。函數式宏難以調試、難以打斷點,不利于定位問題。
對于包含大量語句的宏,在每個調用點都要展開。如果調用點很多,會造成代碼空間的膨脹。
函數式宏缺乏類型檢查的示例代碼:
#define MAX(a, b) (((a) < (b)) ? (b) : (a))int Max(int a, int b) {return (a < b) ? b : a; }int TestMacro(void) {unsigned int a = 1;int b = -1;(void)printf("MACRO: max of a(%u) and b(%d) is %d\n", a, b, MAX(a, b));(void)printf("FUNC : max of a(%u) and b(%d) is %d\n", a, b, Max(a, b));return 0; }由于宏缺乏類型檢查,MAX中的a和b的比較提升為無符號數的比較,結果是a < b。輸出結果是:
MACRO: max of a(1) and b(-1) is -1 FUNC : max of a(1) and b(-1) is 1函數沒有宏的上述缺點。但是,函數相比宏,最大的劣勢是執行效率不高(增加函數調用的開銷和編譯器優化的難度)。
為此,C99標準引入了內聯函數(gcc在標準之前就引入了內聯函數)。
內聯函數跟宏類似,也是在調用點展開。不同之處在于內聯函數是在編譯時展開。
內聯函數兼具函數和宏的優點:
內聯函數/函數執行嚴格的類型檢查
內聯函數/函數的入參求值只會進行一次
內聯函數就地展開,沒有函數調用的開銷
內聯函數比函數優化得更好
對于性能敏感的代碼,可以考慮用內聯函數代替函數式宏。
函數和內聯函數不能完全替代函數式宏,函數式宏在某些場景更適合。
比如,在日志記錄場景下,使用帶可變參和默認參數的函數式宏更方便:
規則6.1 定義宏時,宏參數要使用完備的括號
宏參數在宏展開時只是文本替換,在編譯時再求值。文本替換后,宏包含的語句跟調用點代碼合并。
合并后的表達式因為操作符的優先級和結合律,可能會導致計算結果跟期望的不同,尤其是當宏參數在一個表達式中時。
如下所示,是一種錯誤的寫法:
#define SUM(a, b) a + b // Bad.下面這樣調用宏,執行結果跟預期不符:
100 / SUM(2, 8)?將擴展成?(100 / 2) + 8,預期結果則是100 / (2 + 8)。
這個問題可以通過將整個表示式加上括號來解決,如下所示:
但是這種改法在下面這種場景又有問題:
SUM(1 << 2, 8)擴展成1 << (2 + 8)(因為<<優先級低于+),跟預期結果(1 << 2) + 8不符。
這個問題可以通過將每個宏參數都加上括號來解決,如下所示:
#define SUM(a, b) (a) + (b) // Bad.再看看第三種問題場景:SUM(2, 8) * 10?。擴展后的結果為?(2) + ((8) * 10),跟預期結果(2 + 8) * 10不符。
綜上所述,正確的寫法如下:
#define SUM(a, b) ((a) + (b)) // Good.但是要避免濫用括號。如下所示,單獨的數字或標識符加括號毫無意義。
#define SOME_CONST 100 // Good: 單獨的數字無需括號 #define ANOTHER_CONST (-1) // Good: 負數需要使用括號#define THE_CONST SOME_CONST // Good: 單獨的標識符無需括號下列情況需要注意:
宏參數參與 '#', '##' 操作時,不要加括號
宏參數參與字符串拼接時,不要加括號
宏參數作為獨立部分,在賦值(包括+=, -=等)操作的某一邊時,無需括號
宏參數作為獨立部分,在逗號表達式,函數或宏調用列表中,無需括號
舉例:
#define MAKE_STR(x) #x // x 不要加括號#define HELLO_STR(obj) "Hello, " obj // obj 不要加括號#define ADD_3(sum, a, b, c) (sum = (a) + (b) + (c)) // a, b, c 需要括號;而 sum 無需括號#define FOO(a, b) Bar((a) + 1, b) // a 需要括號;而 b 無需括號規則6.2 包含多條語句的函數式宏的實現語句必須放在 do-while(0) 中
宏本身沒有代碼塊的概念。當宏在調用點展開后,宏內定義的表達式和變量融合到調用代碼中,可能會出現變量名沖突和宏內語句被分割等問題。通過 do-while(0) 顯式為宏加上邊界,讓宏有獨立的作用域,并且跟分號能更好的結合而形成單條語句,從而規避此類問題。
如下所示的宏是錯誤的用法(為了說明問題,下面示例代碼稍不符規范):
// Not Good. #define FOO(x) \(void)printf("arg is %d\n", (x)); \DoSomething((x));當像下面示例代碼這樣調用宏,for循環只執行了宏的第一條語句,宏的后一條語句只在循環結束后執行一次。
for (i = 1; i < 10; i++)FOO(i);用大括號將FOO定義的語句括起來可以解決上面的問題:
#define FOO(x) { \(void)printf("arg is %d\n", (x)); \DoSomething((x)); \ }由于大括號跟分號沒有關聯。大括號后緊跟的分號,是另外一個語句。
如下示例代碼,會出現'懸掛else' 編譯報錯:
正確的寫法是用 do-while(0) 把執行體括起來,如下所示:
// Good. #define FOO(x) do { \(void)printf("arg is %d\n", (x)); \DoSomething((x)); \ } while (0)例外:
包含 break, continue 語句的宏可以例外。使用此類宏務必特別小心。
宏中包含不完整語句時,可以例外。比如用宏封裝 for 循環的條件部分。
非多條語句,或單個 if/for/while/switch 語句,可以例外。
規則6.3 不允許把帶副作用的表達式作為參數傳遞給函數式宏
由于宏只是文本替換,對于內部多次使用同一個宏參數的函數式宏,將帶副作用的表達式作為宏參數傳入會導致非預期的結果。
如下所示,宏SQUARE本身沒有問題,但是使用時將帶副作用的a++傳入導致a的值在SQUARE執行后跟預期不符:
SQUARE(a++)展開后為((a++) * (a++)),變量a自增了兩次,其值為7,而不是預期的6。
正確的寫法如下所示:
b = SQUARE(a); a++; // 結果:a = 6,只自增了一次。此外,如果參數包含函數調用,宏展開后,函數可能會被重復調用。
如果函數執行結果相同,則存在浪費;如果函數多次調用結果不一樣,執行結果可能不符合預期。
建議6.2 函數式宏定義中慎用 return、goto、continue、break 等改變程序流程的語句
宏中使用 return、goto、continue、break 等改變流程的語句,雖然能簡化代碼,但同時也隱藏了真實流程,不易于理解,容易導致資源泄漏等問題。
首先,宏封裝 return 容易導致過度封裝和使用。
如下代碼,status的判斷是主干流程的一部分,用宏封裝起來后,變得不直觀了,閱讀時習慣性把RETURN_IF宏忽略掉了,從而導致對主干流程的理解有偏差。
其次,宏封裝 return 也容易引發內存泄漏。再看一個例子:
#define CHECK_PTR(ptr, ret) do { \if ((ptr) == NULL) { \return (ret); \} \ } while (0)...mem1 = MemAlloc(...); CHECK_PTR(mem1, ERR_CODE_XXX);mem2 = MemAlloc(...); CHECK_PTR(mem2, ERR_CODE_XXX); // Wrong: 內存泄漏如果?mem2?申請內存失敗了,CHECK_PTR?會直接返回,而沒有釋放?mem1。
除此之外,CHECK_PTR?宏命名也不好,宏名只反映了檢查動作,沒有指明結果。只有看了宏實現才知道指針為空時返回失敗。
綜上所述:不推薦宏定義中封裝 return、goto、continue、break 等改變程序流程的語句;
對于返回值判斷等異常處理場景可以例外。
注意:?包含 return、goto、continue、break 等改變流程語句的宏命名,務必要體現對應關鍵字。
建議6.3 函數式宏不超過10行(非空非注釋)
函數式宏本身的一大問題是比函數更難以調試和定位,特別是宏過長,調試和定位的難度更大。
而且宏擴展會導致目標代碼的膨脹。建議函數式宏不要超過10行。
7 變量
在C語言編碼中,除了函數,最重要的就是變量。
變量在使用時,應始終遵循“職責單一”原則。
按作用域區分,變量可分為全局變量和局部變量。
全局變量
盡量不用或少用全局變量。
在程序設計中,全局變量是在所有作用域都可訪問的變量。通常,使用不必要的全局變量被認為是壞習慣。
使用全局變量的缺點:
破壞函數的獨立性和可移植性,使函數對全局變量產生依賴,存在耦合;
降低函數的代碼可讀性和可維護性。當多個函數讀寫全局變量時,某一時刻其取值可能不是確定的,對于代碼的閱讀和維護不利;
在并發編程環境中,使用全局變量會破壞函數的可重入性,需要增加額外的同步保護處理才能確保數據安全。
如不可避免,對全局變量的讀寫應集中封裝。
規則7.1 模塊間,禁止使用全局變量作接口
全局變量是模塊內部的具體實現,不推薦但允許跨文件使用,但禁止作為模塊接口暴露出去。
對全局變量的使用應該盡量集中,如果本模塊的數據需要對外部模塊開放,應提供對應函數接口。
局部變量
規則7.2 嚴禁使用未經初始化的變量
這里的變量,指的是局部動態變量,并且還包括內存堆上申請的內存塊。
因為他們的初始值都是不可預料的,所以禁止未經有效初始化就直接讀取其值。
如果有不同分支,要確保所有分支都得到初始化后才能使用:
void Foo(...) {int data;if (...) {data = 100;}Bar(data); // Bad: 部分分支該值未初始化... }未經初始化就使用,一般靜態檢查工具是可以檢查出來的。
如 PCLint 工具,針對上述兩個例子分別會報錯:
Warning 530: Symbol 'data' (line ...) not initialized Warning 644: Variable 'data' (line ...) may not have been initialized
規則7.3 禁止無效、冗余的變量初始化
如果沒有確定的初始值,而仍然進行初始化,不僅不簡潔,反而不安全,可能會引入更難發現的問題。
常見的冗余初始化:
int cnt = 0; // Bad: 冗余初始化,將會被后面直接覆蓋 ... cnt = GetXxxCnt(); ...對于后續有條件賦值的變量,可以在定義時初始化成默認值
char *buf = NULL; // Good: 這里用 NULL 代表默認值 if (condition) {buf = malloc(MEM_SIZE); } ... if (buf != NULL) { // 判斷是否申請過內存free(buf); }針對大數組的冗余清零,更是會影響到性能。
char buf[VERY_BIG_SIZE] = {0}; memset(buf, 0, sizeof(buf)); // Bad: 冗余清零無效初始化,隱藏更大問題的反例:
void Foo(...) {int data = 0; // Bad: 習慣性的進行初始化UseData(data); // 使用數據,本應該寫在獲取數據后面data = GetData(...); // 獲取數據... }上例代碼,如果沒有賦 0 初始化,靜態檢查工具可以幫助發現“未經初始化就直接使用”的問題。
但因為無效初始化,“使用數據”與“獲取數據”寫顛倒的缺陷,不能被輕易發現。
因此,應該寫簡潔的代碼,對變量或內存塊進行正確、必要的初始化。
C99不再限制局部變量定義必須在語句之前,可以按需定義,即在靠近變量使用的地方定義變量。
這種簡潔的做法,不僅將變量作用域限制更小,而且更方便閱讀和維護,還能解決定義變量時不知該怎么初始化的問題。
如果編譯環境支持,建議按需定義。
例外:
遵從“安全規范”要求,指針變量、表示資源描述符的變量、BOOL變量不作要求。
規則7.4 不允許使用魔鬼數字
所謂魔鬼數字即看不懂、難以理解的數字。
魔鬼數字并非一個非黑即白的概念,看不懂也有程度,需要結合代碼上下文和業務相關知識來判斷
例如數字 12,在不同的上下文中情況是不一樣的:
type = 12;?就看不懂,但?month = year * 12;?就能看懂。
數字 0 有時候也是魔鬼數字,比如?status = 0;?并不能表達是什么狀態。
解決途徑:
對于單點使用的數字,可以增加注釋說明
對于多處使用的數字,必須定義宏或const 變量,并通過符號命名自注釋。
禁止出現下列情況:
沒有通過符號來解釋數字含義,如?#define ZERO 0
符號命名限制了其取值,如?#define XX_TIMER_INTERVAL_300MS 300
8 編程實踐
表達式
建議8.1 表達式的比較,應當遵循左側傾向于變化、右側傾向于不變的原則
當變量與常量比較時,如果常量放左邊,如?if (MAX == v)?不符合閱讀習慣,而?if (MAX > v)?更是難于理解。
應當按人的正常閱讀、表達習慣,將常量放右邊。寫成如下方式:
也有特殊情況,如:if (MIN < v && v < MAX)?用來描述區間時,前半段是常量在左的。
不用擔心將 '==' 誤寫成 '=',因為?if (v = MAX)?會有編譯告警,其他靜態檢查工具也會報錯。讓工具去解決筆誤問題,代碼要符合可讀性第一。
規則8.1 含有變量自增或自減運算的表達式中禁止再次引用該變量
含有變量自增或自減運算的表達式中,如果再引用該變量,其結果在C標準中未明確定義。各個編譯器或者同一個編譯器不同版本實現可能會不一致。
為了更好的可移植性,不應該對標準未定義的運算次序做任何假設。
注意,運算次序的問題不能使用括號來解決,因為這不是優先級的問題。
示例:
x = b[i] + i++; // Bad: b[i]運算跟 i++,先后順序并不明確。正確的寫法是將自增或自減運算單獨放一行:
x = b[i] + i; i++; // Good: 單獨一行函數參數:
Func(i++, i); // Bad: 傳遞第2個參數時,不確定自增運算有沒有發生正確的寫法:
i++; // Good: 單獨一行 x = Func(i, i);建議8.2 用括號明確表達式的操作順序,避免過分依賴默認優先級
可以使用括號強調表達式操作順序,防止因默認的優先級與設計思想不符而導致程序出錯。
然而過多的括號會分散代碼使其降低了可讀性,應適度使用。
當表達式包含不常用,優先級易混淆的操作符時,推薦使用括號,比如位操作符:
c = (a & 0xFF) + b; /* 涉及位操作符,需要括號 */語句
規則8.2 switch語句要有default分支
大部分情況下,switch語句中要有default分支,保證在遺漏case標簽處理時能夠有一個缺省的處理行為。
特例:
如果switch條件變量是枚舉類型,并且 case 分支覆蓋了所有取值,則加上default分支處理有些多余。
現代編譯器都具備檢查是否在switch語句中遺漏了某些枚舉值的case分支的能力,會有相應的warning提示。
建議8.3 慎用 goto 語句
goto語句會破壞程序的結構性,所以除非確實需要,最好不使用goto語句。使用時,也只允許跳轉到本函數goto語句之后的語句。
goto語句通常用來實現函數單點返回。
同一個函數體內部存在大量相同的邏輯但又不方便封裝成函數的情況下,譬如反復執行文件操作, 對文件操作失敗以后的處理部分代碼(譬如關閉文件句柄,釋放動態申請的內存等等), 一般會放在該函數體的最后部分,在需要的地方就goto到那里,這樣代碼反而變得清晰簡潔。實際也可以封裝成函數或者封裝成宏,但是這么做會讓代碼變得沒那么直接明了。
示例:
// Good: 使用 goto 實現單點返回 int SomeInitFunc(void) {void *p1;void *p2 = NULL;void *p3 = NULL;p1 = malloc(MEM_LEN);if (p1 == NULL) {goto EXIT;}p2 = malloc(MEM_LEN);if (p2 == NULL) {goto EXIT;}p3 = malloc(MEM_LEN);if (p3 == NULL) {goto EXIT;}DoSomething(p1, p2, p3);return 0; // OK.EXIT:if (p3 != NULL) {free(p3);}if (p2 != NULL) {free(p2);}if (p1 != NULL) {free(p1);}return -1; // Failed! }類型轉換
建議8.4 盡量減少沒有必要的數據類型默認轉換與強制轉換
當進行數據類型強制轉換時,其數據的意義、轉換后的取值等都有可能發生變化,而這些細節若考慮不周,就很有可能留下隱患。
如下賦值,多數編譯器不產生告警,但值的含義還是稍有變化。
char ch; unsigned short int exam;ch = -1; exam = ch; // Bad: 編譯器不產生告警,此時exam為0xFFFF。推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語言
我的知識小密圈
總結
以上是生活随笔為你收集整理的C语言编程规范 clean code的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IAR8.4.2安装方法
- 下一篇: 2022年考研结束了