# 第?9?章?編碼風格
**目錄**
+ [1\. 縮進和空白](ch09s01.html)
+ [2\. 注釋](ch09s02.html)
+ [3\. 標識符命名](ch09s03.html)
+ [4\. 函數](ch09s04.html)
+ [5\. indent工具](ch09s05.html)
代碼風格好不好就像字寫得好不好看一樣,如果一個公司招聘秘書,肯定不要字寫得難看的,同理,代碼風格糟糕的程序員肯定也是不稱職的。雖然編譯器不會挑剔難看的代碼,照樣能編譯通過,但是和你一個Team的其他程序員肯定受不了,你自己也受不了,寫完代碼幾天之后再來看,自己都不知道自己寫的是什么。[[SICP]](bi01.html#bibli.sicp "Structure and Interpretation of Computer Programs")里有句話說得好:“Thus, programs must be written for people to read, and only incidentally for machines to execute.”代碼主要是為了寫給人看的,而不是寫給機器看的,只是順便也能用機器執行而已,如果是為了寫給機器看那直接寫機器指令就好了,沒必要用高級語言了。代碼和語言文字一樣是為了表達思想、記載信息,所以一定要寫得清楚整潔才能有效地表達。正因為如此,在一個軟件項目中,代碼風格一般都用文檔規定死了,所有參與項目的人不管他自己原來是什么風格,都要遵守統一的風格,例如Linux內核的[[CodingStyle]](bi01.html#bibli.codingstyle "Linux內核源代碼目錄下的Documentation/CodingStyle文件")就是這樣一個文檔。本章我們以內核的代碼風格為基礎來講解好的編碼風格都有哪些規定,這些規定的Rationale是什么。我只是以Linux內核為例來講解編碼風格的概念,并沒有說內核編碼風格就一定是最好的編碼風格,但Linux內核項目如此成功,就足以說明它的編碼風格是最好的C語言編碼風格之一了。
## 1.?縮進和空白
我們知道C語言的語法對縮進和空白沒有要求,空格、Tab、換行都可以隨意寫,實現同樣功能的代碼可以寫得很好看,也可以寫得很難看。例如上一章[例?8.5 “剪刀石頭布”](ch08s05.html#array.scissor)的代碼如果寫成這樣就很難看了:
**例?9.1.?缺少縮進和空白的代碼**
```
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
char gesture[3][10]={"scissor","stone","cloth"};
int man,computer,result, ret;
srand(time(NULL));
while(1){
computer=rand()%3;
printf("\nInput your gesture (0-scissor 1-stone 2-cloth):\n");
ret=scanf("%d",&man);
if(ret!=1||man<0||man>2){
printf("Invalid input! Please input 0, 1 or 2.\n");
continue;
}
printf("Your gesture: %s\tComputer's gesture: %s\n",gesture[man],gesture[computer]);
result=(man-computer+4)%3-1;
if(result>0)printf("You win!\n");
else if(result==0)printf("Draw!\n");
else printf("You lose!\n");
}
return 0;
}
```
一是缺少空白字符,代碼密度太大,看著很費勁。二是沒有縮進,看不出來哪個{和哪個}配對,像這么短的代碼還能湊合著看,如果代碼超過一屏就完全沒法看了。[[CodingStyle]](bi01.html#bibli.codingstyle "Linux內核源代碼目錄下的Documentation/CodingStyle文件")中關于空白字符并沒有特別規定,因為基本上所有的C代碼風格對于空白字符的規定都差不多,主要有以下幾條。
1、關鍵字`if`、`while`、`for`與其后的控制表達式的(括號之間插入一個空格分隔,但括號內的表達式應緊貼括號。例如:
```
while?(1);
```
2、雙目運算符的兩側各插入一個空格分隔,單目運算符和操作數之間不加空格,例如`i?=?i?+?1`、`++i`、`!(i?<?1)`、`-x`、`&a[1]`等。
3、后綴運算符和操作數之間也不加空格,例如取結構體成員`s.a`、函數調用`foo(arg1)`、取數組成員`a[i]`。
4、,號和;號之后要加空格,這是英文的書寫習慣,例如`for?(i?=?1;?i?<?10;?i++)`、`foo(arg1,?arg2)`。
5、以上關于雙目運算符和后綴運算符的規則并沒有嚴格要求,有時候為了突出優先級也可以寫得更緊湊一些,例如`for?(i=1;?i<10;?i++)`、`distance?=?sqrt(x*x?+?y*y)`等。但是省略的空格一定不要誤導了讀代碼的人,例如`a||b?&&?c`很容易讓人理解成錯誤的優先級。
6、由于UNIX系統標準的字符終端是24行80列的,接近或大于80個字符的較長語句要折行寫,折行后用空格和上面的表達式或參數對齊,例如:
```
if?(sqrt(x*x?+?y*y)?>?5.0
&&?x?<?0.0
&&?y?>?0.0)
```
再比如:
```
foo(sqrt(x*x?+?y*y),
a[i-1]?+?b[i-1]?+?c[i-1])
```
7、較長的字符串可以斷成多個字符串然后分行書寫,例如:
```
printf("This is such a long sentence that "
"it cannot be held within a line\n");
```
C編譯器會自動把相鄰的多個字符串接在一起,以上兩個字符串相當于一個字符串`"This is such a long sentence that it cannot be held within a line\n"`。
8、有的人喜歡在變量定義語句中用Tab字符,使變量名對齊,這樣看起來很美觀。
```
→int →a, b;
→double →c;
```
內核代碼風格關于縮進的規則有以下幾條。
1、要用縮進體現出語句塊的層次關系,使用Tab字符縮進,不能用空格代替Tab。在標準的字符終端上一個Tab看起來是8個空格的寬度,如果你的文本編輯器可以設置Tab的顯示寬度是幾個空格,建議也設成8,這樣大的縮進使代碼看起來非常清晰。如果有的行用空格做縮進,有的行用Tab做縮進,甚至空格和Tab混用,那么一旦改變了文本編輯器的Tab顯示寬度就會看起來非常混亂,所以內核代碼風格規定只能用Tab做縮進,不能用空格代替Tab。
2、`if/else`、`while`、`do/while`、`for`、`switch`這些可以帶語句塊的語句,語句塊的{或}應該和關鍵字寫在同一行,用空格隔開,而不是單獨占一行。例如應該這樣寫:
```
if?(...)?{
→語句列表
}?else?if?(...)?{
→語句列表
}
```
但很多人習慣這樣寫:
```
if?(...)
{
→語句列表
}
else?if?(...)
{
→語句列表
}
```
內核的寫法和[[K&R]](bi01.html#bibli.kr "The C Programming Language")一致,好處是不必占太多行,使得一屏能顯示更多代碼。這兩種寫法用得都很廣泛,只要在同一個項目中能保持統一就可以了。
3、函數定義的{和}單獨占一行,這一點和語句塊的規定不同,例如:
```
int?foo(int?a,?int?b)
{
→語句列表
}
```
4、`switch`和語句塊里的`case`、`default`對齊寫,也就是說語句塊里的`case`、`default`標號相對于`switch`不往里縮進,但標號下的語句要往里縮進。例如:
```
→switch?(c)?{
→case 'A':
→ →語句列表
→case 'B':
→ →語句列表
→default:
→ →語句列表
→}
```
用于`goto`語句的自定義標號應該頂頭寫不縮進,而不管標號下的語句縮進到第幾層。
5、代碼中每個邏輯段落之間應該用一個空行分隔開。例如每個函數定義之間應該插入一個空行,頭文件、全局變量定義和函數定義之間也應該插入空行,例如:
```
#include <stdio.h>
#include <stdlib.h>
int g;
double h;
int foo(void)
{
→語句列表
}
int bar(int a)
{
→語句列表
}
int main(void)
{
→語句列表
}
```
6、一個函數的語句列表如果很長,也可以根據相關性分成若干組,用空行分隔。這條規定不是嚴格要求,通常把變量定義組成一組,后面加空行,`return`語句之前加空行,例如:
```
int main(void)
{
→int →a, b;
→double →c;
→語句組1
→語句組2
→return 0;
}
```
## 2.?注釋
單行注釋應采用`/*?comment?*/`的形式,用空格把界定符和文字分開。多行注釋最常見的是這種形式:
```
/*
?*?Multi-line
?*?comment
?*/
```
也有更花哨的形式:
```
/*************\
* Multi-line *
* comment *
\*************/
```
使用注釋的場合主要有以下幾種。
1、整個源文件的頂部注釋。說明此模塊的相關信息,例如文件名、作者和版本歷史等,頂頭寫不縮進。例如內核源代碼目錄下的`kernel/sched.c`文件的開頭:
```
/*
* kernel/sched.c
*
* Kernel scheduler and related syscalls
*
* Copyright (C) 1991-2002 Linus Torvalds
*
* 1996-12-23 Modified by Dave Grothe to fix bugs in semaphores and
* make semaphores SMP safe
* 1998-11-19 Implemented schedule_timeout() and related stuff
* by Andrea Arcangeli
* 2002-01-04 New ultra-scalable O(1) scheduler by Ingo Molnar:
* hybrid priority-list and round-robin design with
* an array-switch method of distributing timeslices
* and per-CPU runqueues. Cleanups and useful suggestions
* by Davide Libenzi, preemptible kernel bits by Robert Love.
* 2003-09-03 Interactivity tuning by Con Kolivas.
* 2004-04-02 Scheduler domains code by Nick Piggin
*/
```
2、函數注釋。說明此函數的功能、參數、返回值、錯誤碼等,寫在函數定義上側,和此函數定義之間不留空行,頂頭寫不縮進。
3、相對獨立的語句組注釋。對這一組語句做特別說明,寫在語句組上側,和此語句組之間不留空行,與當前語句組的縮進一致。
4、代碼行右側的簡短注釋。對當前代碼行做特別說明,一般為單行注釋,和代碼之間至少用一個空格隔開,一個源文件中所有的右側注釋最好能上下對齊。盡管[例?2.1 “帶更多注釋的Hello World”](ch02s01.html#expr.morehelloworld)講過注釋可以穿插在一行代碼中間,但不建議這么寫。內核源代碼目錄下的`lib/radix-tree.c`文件中的一個函數包含了上述三種注釋:
```
/**
* radix_tree_insert - insert into a radix tree
* @root: radix tree root
* @index: index key
* @item: item to insert
*
* Insert an item into the radix tree at position @index.
*/
int radix_tree_insert(struct radix_tree_root *root,
unsigned long index, void *item)
{
struct radix_tree_node *node = NULL, *slot;
unsigned int height, shift;
int offset;
int error;
/* Make sure the tree is high enough. */
if ((!index && !root->rnode) ||
index > radix_tree_maxindex(root->height)) {
error = radix_tree_extend(root, index);
if (error)
return error;
}
slot = root->rnode;
height = root->height;
shift = (height-1) * RADIX_TREE_MAP_SHIFT;
offset = 0; /* uninitialised var warning */
do {
if (slot == NULL) {
/* Have to add a child node. */
if (!(slot = radix_tree_node_alloc(root)))
return -ENOMEM;
if (node) {
node->slots[offset] = slot;
node->count++;
} else
root->rnode = slot;
}
/* Go a level down */
offset = (index >> shift) & RADIX_TREE_MAP_MASK;
node = slot;
slot = node->slots[offset];
shift -= RADIX_TREE_MAP_SHIFT;
height--;
} while (height > 0);
if (slot != NULL)
return -EEXIST;
BUG_ON(!node);
node->count++;
node->slots[offset] = item;
BUG_ON(tag_get(node, 0, offset));
BUG_ON(tag_get(node, 1, offset));
return 0;
}
```
[[CodingStyle]](bi01.html#bibli.codingstyle "Linux內核源代碼目錄下的Documentation/CodingStyle文件")中特別指出,函數內的注釋要盡可能少用。寫注釋主要是為了說明你的代碼“能做什么”(比如函數接口定義),而不是為了說明“怎樣做”,只要代碼寫得足夠清晰,“怎樣做”是一目了然的,如果你需要用注釋才能解釋清楚,那就表示你的代碼可讀性很差,除非是特別需要提醒注意的地方才使用函數內注釋。
5、復雜的結構體定義比函數更需要注釋。例如內核源代碼目錄下的`kernel/sched.c`文件中定義了這樣一個結構體:
```
/*
* This is the main, per-CPU runqueue data structure.
*
* Locking rule: those places that want to lock multiple runqueues
* (such as the load balancing or the thread migration code), lock
* acquire operations must be ordered by ascending &runqueue.
*/
struct runqueue {
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running;
#ifdef CONFIG_SMP
unsigned long cpu_load[3];
#endif
unsigned long long nr_switches;
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;
unsigned long expired_timestamp;
unsigned long long timestamp_last_tick;
task_t *curr, *idle;
struct mm_struct *prev_mm;
prio_array_t *active, *expired, arrays[2];
int best_expired_prio;
atomic_t nr_iowait;
#ifdef CONFIG_SMP
struct sched_domain *sd;
/* For active balancing */
int active_balance;
int push_cpu;
task_t *migration_thread;
struct list_head migration_queue;
int cpu;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;
/* schedule() stats */
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;
/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_local;
#endif
};
```
6、復雜的宏定義和變量聲明也需要注釋。例如內核源代碼目錄下的`include/linux/jiffies.h`文件中的定義:
```
/* TICK_USEC_TO_NSEC is the time between ticks in nsec assuming real ACTHZ and */
/* a value TUSEC for TICK_USEC (can be set bij adjtimex) */
#define TICK_USEC_TO_NSEC(TUSEC) (SH_DIV (TUSEC * USER_HZ * 1000, ACTHZ, 8))
/* some arch's have a small-data section that can be accessed register-relative
* but that can only take up to, say, 4-byte variables. jiffies being part of
* an 8-byte variable may not be correctly accessed unless we force the issue
*/
#define __jiffy_data __attribute__((section(".data")))
/*
* The 64-bit value is not volatile - you MUST NOT read it
* without sampling the sequence number in xtime_lock.
* get_jiffies_64() will do this for you as appropriate.
*/
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;
```
## 3.?標識符命名
標識符命名應遵循以下原則:
1. 標識符命名要清晰明了,可以使用完整的單詞和易于理解的縮寫。短的單詞可以通過去元音形成縮寫,較長的單詞可以取單詞的頭幾個字母形成縮寫。看別人的代碼看多了就可以總結出一些縮寫慣例,例如`count`寫成`cnt`,`block`寫成`blk`,`length`寫成`len`,`window`寫成`win`,`message`寫成`msg`,`number`寫成`nr`,`temporary`可以寫成`temp`,也可以進一步寫成`tmp`,最有意思的是`internationalization`寫成`i18n`,詞根`trans`經常縮寫成`x`,例如`transmit`寫成`xmt`。我就不多舉例了,請讀者在看代碼時自己注意總結和積累。
2. 內核編碼風格規定變量、函數和類型采用全小寫加下劃線的方式命名,常量(比如宏定義和枚舉常量)采用全大寫加下劃線的方式命名,比如上一節舉例的函數名`radix_tree_insert`、類型名`struct radix_tree_root`、常量名`RADIX_TREE_MAP_SHIFT`等。
微軟發明了一種變量命名法叫匈牙利命名法(Hungarian notation),在變量名中用前綴表示類型,例如`iCnt`(i表示int)、`pMsg`(p表示pointer)、`lpszText`(lpsz表示long pointer to a zero-ended string)等。Linus在[[CodingStyle]](bi01.html#bibli.codingstyle "Linux內核源代碼目錄下的Documentation/CodingStyle文件")中毫不客氣地諷刺了這種寫法:“Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged - the compiler knows the types anyway and can check those, and it only confuses the programmer. No wonder MicroSoft makes buggy programs.”代碼風格本來就是一個很有爭議的問題,如果你接受本章介紹的內核編碼風格(也是本書所有范例代碼的風格),就不要使用大小寫混合的變量命名方式<sup>[[19](#ftn.id2738703)]</sup>,更不要使用匈牙利命名法。
3. 全局變量和全局函數的命名一定要詳細,不惜多用幾個單詞多寫幾個下劃線,例如函數名`radix_tree_insert`,因為它們在整個項目的許多源文件中都會用到,必須讓使用者明確這個變量或函數是干什么用的。局部變量和只在一個源文件中調用的內部函數的命名可以簡略一些,但不能太短。盡量不要使用單個字母做變量名,只有一個例外:用`i`、`j`、`k`做循環變量是可以的。
4. 針對中國程序員的一條特別規定:禁止用漢語拼音做標識符,可讀性極差。
* * *
<sup>[[19](#id2738703)]</sup> 大小寫混合的命名方式是Modern C++風格所提倡的,在C++代碼中很普遍,稱為CamelCase),大概是因為有高有低像駝峰一樣。
## 4.?函數
每個函數都應該設計得盡可能簡單,簡單的函數才容易維護。應遵循以下原則:
1. 實現一個函數只是為了做好一件事情,不要把函數設計成用途廣泛、面面俱到的,這樣的函數肯定會超長,而且往往不可重用,維護困難。
2. 函數內部的縮進層次不宜過多,一般以少于4層為宜。如果縮進層次太多就說明設計得太復雜了,應考慮分割成更小的函數(Helper Function)來調用。
3. 函數不要寫得太長,建議在24行的標準終端上不超過兩屏,太長會造成閱讀困難,如果一個函數超過兩屏就應該考慮分割函數了。[[CodingStyle]](bi01.html#bibli.codingstyle "Linux內核源代碼目錄下的Documentation/CodingStyle文件")中特別說明,如果一個函數在概念上是簡單的,只是長度很長,這倒沒關系。例如函數由一個大的`switch`組成,其中有非常多的`case`,這是可以的,因為各`case`分支互不影響,整個函數的復雜度只等于其中一個`case`的復雜度,這種情況很常見,例如TCP協議的狀態機實現。
4. 執行函數就是執行一個動作,函數名通常應包含動詞,例如`get_current`、`radix_tree_insert`。
5. 比較重要的函數定義上側必須加注釋,說明此函數的功能、參數、返回值、錯誤碼等。
6. 另一種度量函數復雜度的辦法是看有多少個局部變量,5到10個局部變量已經很多了,再多就很難維護了,應該考慮分割成多個函數。
## 5.?indent工具
`indent`工具可以把代碼格式化成某種風格,例如把[例?9.1 “缺少縮進和空白的代碼”](ch09s01.html#codingstyle.badcode)格式化成內核編碼風格:
```
$ indent -kr -i8 main.c
$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
char gesture[3][10] = { "scissor", "stone", "cloth" };
int man, computer, result, ret;
srand(time(NULL));
while (1) {
computer = rand() % 3;
printf
("\nInput your gesture (0-scissor 1-stone 2-cloth):\n");
ret = scanf("%d", &man);
if (ret != 1 || man < 0 || man > 2) {
printf("Invalid input! Please input 0, 1 or 2.\n");
continue;
}
printf("Your gesture: %s\tComputer's gesture: %s\n",
gesture[man], gesture[computer]);
result = (man - computer + 4) % 3 - 1;
if (result > 0)
printf("You win!\n");
else if (result == 0)
printf("Draw!\n");
else
printf("You lose!\n");
}
return 0;
}
```
`-kr`選項表示K&R風格,`-i8`表示縮進8個空格的長度。如果沒有指定`-nut`選項,則每8個縮進空格會自動用一個Tab代替。注意`indent`命令會直接修改原文件,而不是打印到屏幕上或者輸出到另一個文件,這一點和很多UNIX命令不同。可以看出,`-kr -i8`兩個選項格式化出來的代碼已經很符合本章介紹的代碼風格了,添加了必要的縮進和空白,較長的代碼行也會自動折行。美中不足的是沒有添加適當的空行,因為`indent`工具也不知道哪幾行代碼在邏輯上是一組的,空行還是要自己動手添,當然原有的空行肯定不會被`indent`刪去的。
如果你采納本章介紹的內核編碼風格,基本上`-kr -i8`這兩個參數就夠用了。`indent`工具也有支持其它編碼風格的選項,具體請參考Man Page。有時候`indent`工具的確非常有用,比如某個項目中途決定改變編碼風格(這很少見),或者往某個項目中添加的幾個代碼文件來自另一個編碼風格不同的項目,但絕不能因為有了`indent`工具就肆無忌憚,一開始把代碼寫得亂七八糟,最后再依靠`indent`去清理。
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引