# 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 上, 一目了然哪邊還需要修改, 可以多加利用.