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