# C Coding Style Guild ###### Author: Ted ###### Time: 2023/12/03 ###### Version: v0 ## Background 本指南列出了在Xilinx SDK C code撰寫上的注意事項, 主要參考了以下文件 - Xilinx SDK driver [[github]](https://github.com/Xilinx/embeddedsw/tree/master/XilinxProcessorIPLib/drivers) - Linux kernel coding sytle [[原文]](https://docs.kernel.org/process/coding-style.html) [[中文版]](https://docs.kernel.org/translations/zh_TW/process/coding-style.html) - Google C++ coding style [[原文]](https://google.github.io/styleguide/cppguide.html) [[中文版]](https://tw-google-styleguide.readthedocs.io/en/latest/). --- 在公司過往的project中, coding style 時常改變, 甚至一個 project 有多種 coding style, 要維護或開發舊 project 時就會對變數命名產生疑惑, 希望此指南完成後的所有 project 皆遵循一個固定的 coding style , 減少產生**命名困惑**的情況發生. 代碼風格因人而異, 但任何事情都必須遵循一定的原則, 至少希望在進行公司的project時使用這裡的代碼風格. ## 1. 縮進 任何縮進皆使用 tab. 雖然很多命名法則都推薦使用空白鍵, 但 Xilinx 使用 tab 來進行縮排, 為了統一, 請使用 tab 而非按下4次空白鍵. 縮進的全部意義就在於清楚的定義一個控制塊起止於何處。尤其是當你盯着你的螢幕連續看了 20 小時之後, 你會希望有良好的縮進來讓你看清楚程式區塊. ```c= if (a > b) { return a; } else { return b; } ``` switch語句使用兩次縮進 ```c= switch(Guest) { case 'R': case 'A': case 'I': case 'T': case 'E': case 'K': Employees = 30; break; default: break; } ``` ## 2. 每行程式碼長度上限 每一行程式碼的長度**盡量**不超過 80 個字元. 要強制每行程式碼都小於 80 個字元這個當然是有爭議的, 80 字元的限制是上個世紀 60 年代大型主機的顯示缺陷, 現代的螢幕更寬,可以很輕鬆地顯示更多程式碼, 但很多現有程式碼, 包括 Xilinx 都已經遵守這一項規則, 因此一致性更重要. 但以下情況可以有彈性地超過這個限制: - 如果該行是註解, 且為了不妨礙閱讀、方便複製貼上. 例如: 命令列指令的範例、URL等. - 如果該行是include陳述句. - 如果該行是define Guard. ```c= #ifndef _RAITEK_RAITEK_RAITEK_RAITEK_RAITEK_RAITEK_H_ #define _RAITEK_RAITEK_RAITEK_RAITEK_RAITEK_RAITEK_H_ ... #endif // _RAITEK_RAITEK_RAITEK_RAITEK_RAITEK_RAITEK_H_ ``` ## 2. 大括號的位置 把起始大括號放在行尾,而把結束大括號放在行首. 這適用於所有的非函數語句塊(if, switch, for, while, do).比如 ```c= if (condition) { return; } ``` ```c= switch (Action) { case ADD: break; case REMOVE: x = 0; break; default: x = 1; break; } ``` 不要把多個語句放在一行裏, 除非你有什麼東西要隱藏, 就算語句只有一行, 也依然要使用大括號 [[相關新聞: Apple的ssltls bug]](https://developer.aliyun.com/article/131178): ```c= // incorrect if (Apple == 0) do_this; // correct if (Apple == 0) { do_this; } ``` 不過, 有一個例外, 那就是函數:函數的起始大括號放置於下一行的開頭, 所以: ```c= int Function(int X) { ... } ``` 注意結束大括號獨自佔據一行, 除非它後面跟着同一個語句的剩餘部分, 也就是 do 語句中的 while 或者 if 語句中的 else , 像這樣: ```c= do { body of do-loop } while (condition); ``` ```c= if (X == Y) { .. } else if (X > Y) { ... } else { .... } ``` ## 3. 空格的位置 空格的使用方式主要取決於它是用於函數還是關鍵字. (大多數)關鍵字後要加一個空格. 值得注意的例外是 sizeof, typeof, alignof 和 __attribute\__,這 些關鍵字某些程度上看起來更像函數 (它們在 Xilinx 裏也常常伴隨小括號而使用, 儘管在 C 裏這樣的小括號不是必需的). 所以在這些關鍵字之後放一個空格: ```c= if, switch, case, for, do, while ``` 但是不要在 sizeof, typeof, alignof 或者 __attribute\__ 這些關鍵字之後放空格. 例如: ```c= S = sizeof(struct File); ``` 不要在小括號裏的表達式兩側加空格. 底下舉個錯誤的例子: ```c= S = sizeof( struct File ); ``` 當聲明指針類型或者返回指針類型的函數時, * 的首選使用方式是使之靠近變量名或者函數名, 而不是靠近類型名. 例子: ```c= char *String; unsigned long long Memparse(char *Ptr, char **Retptr); char *Match(substring_t *S); ``` 在大多數二元和三元操作符兩側使用一個空格, 例如下面所有這些操作符: ```c= = + - < > * / % | & ^ <= >= == != ? : ``` 但是一元操作符後不要加空格: ```c= & * + - ~ ! sizeof typeof alignof __attribute__ defined ``` 後綴與前綴的自加和自減一元操作符前後都不加空格: ```c= cnt++ --number ``` `.` 和 `->` 結構體成員操作符前後不加空格 ## 4. 命名 C 是一個簡樸的語言, 你的命名也應該這樣. 不應該使用類似 ThisVariableIsATemporaryCounter 這樣華麗的名字. 請直接稱那個變量爲 Tmp , 這樣寫起來會更容易,而且至少不會令其難於理解。 除此之外, 變量名應該簡短且能夠清楚表達相關的涵義, 讓團隊中共同開發或整合的同事在 trace code 時不會那麼費力, 同時在命名時不要使用只有自己才知道是甚麼意思的變數. ### 4.1 檔案名稱命名 不管為資料夾名稱還是.c檔或.h檔, 名稱全部使用小寫命名, 但在一些必要的地方允許使用下劃線`_`來清楚的表達變數的意思. ```c= main.c main.h fms.c fms.h sdcard_transfer.h ``` ### 4.2 #define 與 MACRO 命名 使用大寫, 但在一些必要的地方允許使用下劃線`_`來清楚的表達變數的意思: ```c= #define UARTLITE_0_ID 0 #define PASSWORD_MASK 1 ``` ### 4.3 函數命名 在命名時需要注意函數是否足夠簡短與足夠表達含意外, 命名方式與 Xilinx 一致, 使用[大駝峰命名法](https://zh.wikipedia.org/zh-tw/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB) (upper camel case), 可以使用下劃線`_`, 但只能用在`功能`與`函數目的`間 例如: ```c= int Timer_Init(); // Timer為硬體功能, Init為函數目的 void ADC_GetVal(); // ADC為硬體功能, GetVal為函數目的 int FMS_EraseFile(); // FMS為軟體功能(File Management System), EraseFile為函數目的 ``` 在命名 `函數目的` 時, 如果函數有包含動作(比如: Read, Write), 則動作在前面. 如果該函數只有被該檔案中的其他函數用到, 請在前面加上 **static** 關鍵字, 讓其他人 trace code 時更輕鬆. ### 4.4 變數命名 (區域) 與 Xilinx 一致, 使用[大駝峰命名法](https://zh.wikipedia.org/zh-tw/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB) (upper camel case), 且不使用任何下劃線`_`, 在命名時也需要注意變數是否足夠簡短與足夠表達含意. 除了常見的縮寫或團隊都知道的縮寫, 除此之外請不要自行使用縮寫來命名變數: ```c= int Status; // OK int ReadIdx; // OK, Idx 代表 Index, 是一個常見的縮寫 int ErrCnt; // OK, CMD 帶表 Index, 也是一個常見的縮寫 int Price_Count_Reader; // Not OK, 不夠簡短, 且有使用下劃線 int n_comp_conns; // ???????? 沒人知道這是甚麼 ``` 此命名方式也適用於: `union`, `struct`. 對於[匈牙利命名法](https://zh.wikipedia.org/zh-tw/%E5%8C%88%E7%89%99%E5%88%A9%E5%91%BD%E5%90%8D%E6%B3%95), 其帶來的缺點比優點要多得多, Linux 的作者做出了以下的批評, 以下節錄原文. ``` Encoding the type of a function into the name (so-called Hungarian notation) is asinine - the compiler knows the types anyway and can check those, and it only confuses the programmer. ``` ### 4.5 變數命名 (全域) 與區域變數命名方式一致, 但加上`g_`前綴, 例如: ```c= int g_SysTime; XUartLite g_UartLite; ``` ## 5. Coding 原則 以下說明了幾項在 coding 時的大方向. ### 5.1 typedef 不要使用類似 `vps_t` 之類的東西, 對結構體和指針指用 typedef 是一個錯誤, 當你在代碼裏看到: ```c= vps_t a; ``` 這代表甚麼意思呢? 相反, 如果是這樣: ```c= struct virtual_container *a; ``` 你就知道`a`是甚麼意思了. 很多人認爲 typedef 能`提高可讀性`. 實際不是這樣的. 基本的規則就是**永遠不要**使用 typedef. ### 5.2 函數 #### 5.2.1 長度 函數應該簡短而漂亮, 並且只完成一件事情. 函數應該可以一屏或者兩屏顯示完 (Xilinx SDK 一屏通常為50行, 所以函數通常都在0 ~ 100行內完成), 只做一件事情, 而且把它做好. 一個函數的最大長度是和該函數的複雜度和縮進級數成反比的. 所以, 如果你只有一個很長 (但是簡單) 的 case 語句的函數, 而且你需要在每個 `case` 裏做很多很小的事情, 這樣的函數儘管很長, 但也是可以的. 不過, 如果你有一個複雜的函數, 而且你懷疑第一次看到這個函數的人會搞不清楚這個函數的目的, 你應該嚴格遵守前面提到的長度限制. 寫上註解並爲之取個具描述性的名字. #### 5.2.2 變量數量 函數的另外一個衡量標準是變量的數量. 函數中出現的變量數量不應超過 5-10 個, 否則你的函數 就有問題了. 重新考慮一下你的函數, 把它拆分成更小的函數. 人的大腦一般可以輕鬆的同時跟蹤 7 個不同的事物, 如果再增多的話, 就會糊塗了 (且會更難debug). 即便你聰穎過人, 你也可能會記不清你 2 個星期前做過的事情. #### 5.2.3 宣告 在.h檔宣告函數時建議也包含參數名稱和它們的數據類型. #### 5.2.4 goto 禁止使用 goto, 使用goto除了會讓 trace code 變得困難外, 並無其他優點. (在 Linux 的 source code 中會使用 goto, 但使用方式非常有限且謹慎, SDK 並中並不需要) SDK 中, 寫得夠好的函數是不需要用到goto的. #### 5.2.4 return 盡量減少 return 的次數, 且 return 的地方盡量在函數結尾. ```c= // 多個 return 語句 int foo(int x, int y) { if (x < 0) return -1; if (y < 0) return -2; // 更多邏輯... return x + y; } // 集中在函數結尾 int Foo(int X, int Y) { int Res; if (X < 0) { Res = -1; } else if (Y < 0) { Res = -2; } else { // 更多邏輯... Res = X + Y; } return Res; } ``` - 多個 return 語句會使函數流程變得雜亂, 容易讓人難以理解程式碼的執行邏輯. 將 return 集中在函數結尾,可以讓程式碼更加簡潔、清晰, 易於理解和維護。 - 在函數中間 return 可能會導致某些代碼永遠不會被執行, 從而引起潛在的邏輯錯誤. 將 return 語句集中在函數結尾, 可以避免這種情況發生. - 在某些特殊情況下, 如硬體初始化或錯誤處理, 仍然可能需要在函數中間使用 return 語句。但總的來說, 盡量減少 return 的次數, 並將其集中在函數結尾, 可以提高代碼質量, 是一個值得遵循的好習慣. ### 5.3 While While 中, 就算不執行任何函數, 也需空一行加上`;` ```c= while (RecvCMD()) { ; } ``` 除了主函數外, 永遠不要使用 while(1) ```c= while (1) { do_something...; if (condition) { break; } } ``` 請使用 ```c= while (condition) { do_something...; } ``` 以下舉例: ```c= while (1) { char input[10]; scanf("%s", input); if (strcmp(input, "exit") == 0) { break; } // 處理其他輸入 } //////////////////////////////////////////////////// char input[10]; while (strcmp(input, "exit") != 0) { scanf("%s", input); // 處理輸入 } ``` 第二種寫法不僅更加清晰易懂, 而且在控制循環終止條件方面也更加簡潔和直接。 ### 5.4 檔案 將函數分別別類而不是全部塞在 main.c 中, 例如: - Uart_Send() - Uart_Recv() - Uart_ChangeBaudRate() 這三個函數應該放在同一個名叫 uart 的資料夾中並在需要的地方 include .h檔 ### 5.5 全域變數 宣告全域變數時應該要非常謹慎, 且數量應該要越少越好. 當某個函數穿插著各種全域變數時, 你會非常難開發、追蹤和 debug, 因為你沒辦法確認你眼前的全域變數有沒有被其他函數改過, 只能進 debug mode來一步一步追蹤, 會造成時間大量的浪費. 所以當你宣告了一個全域變數時需要謹慎地像發射核彈頭一樣: 不到萬不得已, 絕對不做. ## 6. 註解 註解雖然寫起來很痛苦, 但對保證程式碼可讀性至關重要. 下面的規則描述了如何註釋以及在哪兒註釋. 當然也要記住: 註釋固然很重要, 但最好的程式碼本身應該是自文檔化. 有意義的函數名和變數名, 遠勝過要用註釋解釋的含糊不清的名字. 你寫的註解是給下一個需要理解你的程式碼的人看的. 慷慨些吧, 下一個人可能就是你! ### 6.1 註解風格 要用 `//` 還是 `/* */` 都可以, 但 `//` 更常用, 且統一使用 **英文** 寫註解. 需注意的是, 在使用 `//` 時, 註解內容須空一格. 如果在句尾的話則在 `//` 前空兩格 ```c= int NumReader; // Number of people who have read this document ``` ### 6.2 struct 註解 每個自行定義的 struct 都必須附帶一份註解. 不管是在 struct 的上方有一塊註解 ```c= // Boss: The person who manages this company. struct Raitek { char *Boss; int NumEmployees; }; ``` 還是在 struct's member 後寫註解 ```c= struct Raitek { char *Boss; // The person who manages this company. int NumEmployees; }; ``` 都可以, 但至少需要確保有註解或相關文件. ### 6.3 函數註解 程式碼中巧妙的, 晦澀的, 有趣的, 重要的地方加以註解. 另外, 如果函數內使用到了 [**magic number**](https://ithelp.ithome.com.tw/articles/10207871), 則一定要有註解解釋. ```c= int main() { int Age = 18; int RetireAge = 65; int YearsLeft = RetireAge - Age; // Magic numbers used here if (YearsLeft >= 47) { printf("You have a long way to go before retirement!\n"); } else if (YearsLeft >= 25) { printf("You're halfway through your career.\n"); } else { printf("You're nearing retirement age!\n"); } // More magic numbers int Salary = 5000; int Raise = 500; Salary += Raise; printf("Your new salary is $%d\n", Salary); return 0; } ``` 上述的例子中各種數字, 都會讓人摸不著頭腦, 你不會想在 3 個月後回來維護專案時看到這段程式碼的. ### 6.4 全域變數註解 所有全域變數都要註解說明含義及用途. ```c= // The total number of tests cases that we run through in this regression test. const int g_NumTestCases = 6; ``` ### 6.5 TODO 對那些臨時的, 短期的解決方案, 或已經夠好但仍不完美的程式碼使用 `TODO` 註解. 在 Xilinx SDK, 如果有用TODO的話會有個藍色的註記在程式碼右邊的 Slider Bar 上, 一目了然哪邊還需要修改, 可以多加利用.