内核编码约定¶
缩进和空白¶
C 语法本身不强制缩进和空白, 但风格差的代码难读也难维护. 下面以 Linux 内核风格为主整理常见约定.
空白¶
-
if/while/for与后面的(之间加一个空格, 括号内侧不加空格:while (1); -
双目运算符两侧加空格; 单目运算符与操作数之间不加空格:
i = i + 1; ++i; !(i < 1); -x; &a[1]; -
后缀运算符与操作数之间不加空格:
s.a,foo(arg1),a[i]. -
,和;之后加空格, 模仿英文书写习惯:for (i = 1; i < 10; i++) foo(arg1, arg2); -
为突出优先级可写得更紧凑, 但不要误导读者. 例如
a || b && c容易让人看错优先级. -
接近或超过 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]); -
长字符串可拆成相邻多个字符串分行书写, 编译器会自动拼接:
printf("This is such a long sentence that " "it cannot be held within a line\n"); -
变量定义可用 Tab 对齐变量名, 便于阅读:
int a, b; double c;
缩进¶
-
用缩进体现语句块层次; 只用 Tab 缩进, 不要用空格代替 Tab. 建议编辑器把 Tab 显示宽度设为 8, 与终端一致. 空格与 Tab 混用时, 一旦改显示宽度就会乱.
-
if/else/while/do/for/switch的{}与关键字同一行 (K&R / 内核风格), 用空格隔开:if (...) { /* ... */ } else if (...) { /* ... */ }{单独占一行也很常见; 同一项目内保持统一即可. -
函数定义 的
{}单独占一行 (与控制语句不同):int foo(int a, int b) { /* ... */ } -
switch与其中的case/default对齐 (标号相对switch不缩进), 标号下的语句再缩进一层.goto的自定义标号顶格写.switch (c) { case 'A': /* ... */ case 'B': /* ... */ default: /* ... */ } -
逻辑段落之间空一行分隔: 函数与函数之间, 头文件 / 全局变量 / 函数定义之间.
-
函数体较长时, 可按相关性分组并用空行隔开. 常见做法: 变量定义后空一行,
return前空一行.
#include <stdio.h>
#include <stdlib.h>
int g;
double h;
int foo(void)
{
/* ... */
}
int main(void)
{
int a, b;
double c;
/* 语句组 */
return 0;
}
indent¶
indent 按一组选项重排 C 代码. 它会直接修改原文件, 并留下 *.c~ 备份.
内核自带的格式化脚本是 scripts/Lindent, 现行参数为:
indent -npro -kr -i8 -ts8 -sob -l80 -ss -ncs -cp1 -il0 file.c
-
-npro: 不读~/.indent.pro, 避免本机配置盖过这组参数 -
-kr: K&R 风格, 控制语句的{与关键字同行 -
-i8 -ts8: 缩进 8 列, 用 Tab. 未加-nut时, 每 8 个缩进空格会换成一个 Tab -
-l80: 行宽 80 -
-sob: 删掉可有可无的空行. 按逻辑分组的空行仍要自己加 -
-ss:for (;;)这类语句里, 分号前留一个空格 -
-ncs: 强制转换后不加空格, 写成(int)x -
-cp1:#else/#endif旁边的注释放在第 1 列 -
-il0:case与switch对齐, 不再多缩进一层
GNU indent 另有预设 -linux, 一次打开它认为的 "Linux 风格". 其中 -i8 -ts8 -l80 -ncs -sob 与上面相同, 但有两处和 Lindent 不一致: -il1 会让 case 比 switch 多缩进一列, -cp33 把 #else / #endif 旁的注释放到第 33 列. 要对齐内核, 用 Lindent 那一行, 不要用 -linux.
注释¶
单行注释用 /* comment */, 界定符与文字之间加空格. 多行注释常见写法:
/*
* Multi-line
* comment
*/
使用场合¶
-
文件顶部注释: 说明模块信息 (文件名, 作者, 版本历史等), 顶格写不缩进.
-
函数注释: 说明功能, 参数, 返回值, 错误码等, 写在函数定义正上方, 与定义之间不留空行, 顶格写. 内核常用类似 kernel-doc 的形式:
/** * 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(...) { /* ... */ } -
语句组注释: 对相对独立的一组语句做说明, 写在语句组正上方, 不留空行, 缩进与当前语句组一致.
-
行右侧简短注释: 对当前行做特别说明, 与代码之间至少隔一个空格; 同一文件内右侧注释最好上下对齐. 不建议把注释穿插在一行代码中间.
-
复杂结构体: 比函数更需要注释, 可说明用途, 锁定规则, 字段含义等.
-
复杂宏与变量声明: 也需要注释, 尤其是易误用或有隐含约束的定义.
原则¶
函数内注释应尽可能少. 注释主要说明代码 能做什么 (接口与意图), 而不是 怎样做. 代码写得足够清晰时, "怎样做" 应一目了然; 需要大段注释才能解释清楚, 往往说明可读性不够. 只有特别需要提醒注意的地方, 才在函数内加注释.
命名原则¶
清晰可读¶
命名要清楚, 可用完整单词或易懂缩写. 短词常去元音, 长词常取前几个字母. 常见缩写惯例:
| 原词 | 缩写 | 原词 | 缩写 |
|---|---|---|---|
| count | cnt | block | blk |
| length | len | window | win |
| message | msg | number | nr |
| temporary | temp / tmp | transmit | xmt (词根 trans → x) |
| internationalization | i18n |
大小写与分隔¶
内核风格约定:
-
变量 / 函数: 全小写 + 下划线, 如
radix_tree_insert -
类型 (
struct/enum的名字): 同样全小写 + 下划线, 如struct radix_tree_root,enum dma_data_direction. 自定义类型也不用驼峰 -
常量 (宏, 枚举值): 全大写 + 下划线, 如
RADIX_TREE_MAP_SHIFT,DMA_BIDIRECTIONAL. 枚举的类型名仍是小写, 只有枚举常量才大写
不要用匈牙利命名法 (在名字里塞类型前缀, 如 iCnt, pMsg, lpszText).
Linus 对匈牙利命名法的评价
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.
内核风格也不采用大小写混合的CamelCase(驼峰命名), 那是Modern C++里更常见的写法. 教材里的 VertexType, MGraph 属于这一类, 不是内核约定.
类型与typedef¶
名字用 snake_case 还不够, 内核几乎不为结构体和指针做 typedef. 看见变量声明时, 要立刻知道它是结构体, 指针还是整数. 写成带 tag 的 struct, 用的时候把 struct 写出来.
例如下面代码定义了一个无向图的邻接矩阵表示:
struct mgraph {
char vexs[MAX_VERTEX_NUM];
int edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
int vexnum, arcnum;
};
struct mgraph g; /* 对 */
在408中或者其他书中, 我们通常会这样定义一个图的结构体:
typedef struct {
/* ... */
} mgraph_t; /* 错: 文档点名反对 vps_t 这种写法 */
typedef struct mgraph *mgraph_p; /* 更错: 把指针藏进类型名 */
但在内核中, 这样写是不受欢迎的.
为什么不受欢迎
因为这种写法把类型的真实面目藏了起来. 内核文档的对比是: 看见 vps_t a 你不知道 a 是结构体, 整数还是指针; 写成 struct virtual_container *a 一眼就知道.
回到上面的无向图的邻接矩阵, 它的成员本来就要直接读写 (g.edge[i][j]), 并不是 pte_t 那种「布局随架构变, 只许走访问器」的不透明对象, 用 typedef 装抽象非但没有好处, 弊端倒是不少:
-
mgraph_t a: 看起来像小整数.MAX_VERTEX_NUM为 500 时, 光edge[500][500]就是约 1 MB. 类型被藏住后, 有人会写成void foo(mgraph_t g)按值传入, 整张矩阵砸在栈上. -
mgraph_p p: 看起来像对象, 其实是指针. 容易漏查NULL; 再写成mgraph_p *pp已经是二级指针, 看起来却不像. -
匿名
typedef struct { ... } mgraph_t没有 tag, 头文件里没法struct mgraph;做前置声明.
_t 还是 POSIX 留给 size_t, pid_t 的后缀, 用户类型再叫 mgraph_t 既和文档点名批评的 vps_t 同款, 也容易和系统类型撞名.
内核实体都是这个样子: struct task_struct, struct list_head, struct file_operations.
-
完全不透明, 只能用访问器碰的对象, 如
pte_t -
需要抽象位宽的整数, 如
u8/u32, 或配置相关可能是int也可能是long的类型 -
偶尔的函数指针类型
typedef char vertex_type 这种别名一般也不写: char 已经够清楚, 别名没有藏起可移植差异. 真要抽象成「以后可能改底层类型」, 才考虑 typedef, 名字仍用 snake_case. 透明结构体不要加 _t (_t 还和 POSIX 保留后缀撞车).
| 内核 | 教材常见 | |
|---|---|---|
| 结构体 | struct mgraph | typedef struct { ... } MGraph; |
| 成员 / 函数 | vex_num, first_neighbor() | vexnum, FirstNeighbor |
| 宏 / 枚举值 | MAX_VERTEX_NUM, GRAPH_DIRECTED | 同样大写 |
| 枚举类型名 | enum graph_kind | 有时 GraphKind |
其他约定¶
-
全局变量 / 全局函数: 名字要详细, 宁可多几个单词和下划线, 能一眼看懂用途 (如
radix_tree_insert). -
局部变量 / 单文件内部函数: 可简略一些, 但不要太短.
-
尽量不使用单字母变量; 循环变量
i,j,k除外. -
禁用汉语拼音标识符.
函数设计原则¶
单个函数应尽量简单, 简单才容易维护.
单一职责¶
一个函数只做好一件事. 不要做成"万能工具箱": 用途太广往往又长又难复用, 也难修改.
控制复杂度¶
-
缩进层次: 一般少于 4 层. 层次过多说明设计过复杂, 应拆成更小的辅助函数 (Helper Function).
-
函数长度: 建议在 24 行标准终端上不超过两屏; 超过就考虑拆分. 例外: 概念上很简单、只是篇幅长的函数可以接受. 典型例子是一个大
switch带很多case(各分支互不影响, 整体复杂度约等于其中一个分支), 如 TCP 状态机. -
局部变量数量: 5 到 10 个已经偏多, 再多就难维护, 应考虑拆成多个函数.
命名与注释¶
-
函数表示一个动作, 名字通常应含动词, 如
get_current,radix_tree_insert. -
比较重要的函数定义上方必须加注释, 说明功能, 参数, 返回值, 错误码等.
函数修饰¶
-
没有参数时写成
(void). 声明里的()表示参数未说明, 不是没有参数.int foo(void); int foo(void) { return 0; } /* not this! */ int foo(); int foo() { return 0; }函数定义里的空列表在 C99 中虽然等于没有参数, 内核仍要求定义和声明都写
(void). Checkpatch会把int foo ()报成FUNCTION_WITHOUT_ARGS. -
只在本文件使用的函数应使用
static修饰.static int helper(void) { return 0; }不加
static表示该函数具有外部链接. 内核编译打开了-Wmissing-prototypes选项, 具有外部链接的函数必须先有原型, 一般放在头文件里.