代码风格
备注
Armino 以 Linux kernel coding style 为基础,对部分规范进行了调整或者删除。
缩进
除了注释、文档之外,不要使用空格来缩进,使用制表符进行缩进。
在 switch 语句中消除多级缩进的首选的方式是让 switch
和从属于它的 case
标签对齐于同一列,而不要 两次缩进
case
标签。比如:
switch (suffix) {
case 'K':
case 'k':
mem <<= 10;
/* fall through */
default:
break;
}
不要把多个语句放在一行里,也不要在一行里放多个赋值语句。
不要在行尾留空格。
把长的行和字符串打散
每一行的长度的限制是 80 列,我们强烈建议您遵守这个惯例。
长于 80 列的语句要打散成有意义的片段。除非超过 80 列能显著增加可读性,并且不 会隐藏信息。子片段要明显短于母片段,并明显靠右。这同样适用于有着很长参数列表 的函数头。然而,绝对不要打散对用户可见的字符串,例如 printk 信息,因为这样就 很难对它们 grep。
大括号和空格的放置
把起始大括号放在行尾,而把结束大括号放在行首,所以:
if (x is true) {
we do y
}
这适用于所有的非函数语句块 (if, switch, for, while, do)。比如:
switch (action) {
case KOBJ_ADD:
return "add";
default:
return NULL;
}
不过,有一个例外,那就是函数:函数的起始大括号放置于下一行的开头,所以:
int function(int x)
{
body of function
}
注意结束大括号独自占据一行,除非它后面跟着同一个语句的剩余部分,也就是 do 语 句中的 “while” 或者 if 语句中的 “else”,像这样:
do {
body of do-loop
} while (condition);
和
if (x == y) {
..
} else if (x > y) {
...
} else {
....
}
当只有一个单独的语句的时候,不用加不必要的大括号。
if (condition)
action();
和
if (condition)
do_this();
else
do_that();
这并不适用于只有一个条件分支是单语句的情况;这时所有分支都要使用大括号:
if (condition) {
do_this();
do_that();
} else {
otherwise();
}
空格
空格使用方式 (主要) 取决于它是用于函数还是关键字。(大多数) 关键字 后要加一个空格。值得注意的例外是 sizeof, typeof, alignof 和 __attribute__,这 些关键字某些程度上看起来更像函数。
所以在这些关键字之后放一个空格:
if, switch, case, for, do, while
但是不要在 sizeof, typeof, alignof 或者 __attribute__ 这些关键字之后放空格。 例如,
s = sizeof(struct file);
不要在小括号里的表达式两侧加空格。这是一个 反例 :
s = sizeof( struct file );
当声明指针类型或者返回指针类型的函数时, *
的首选使用方式是使之靠近变量名
或者函数名,而不是靠近类型名。例子:
char *armino_banner;
unsigned long long memparse(char *ptr, char **retptr);
char *match_strdup(substring_t *s);
在大多数二元和三元操作符两侧使用一个空格,例如下面所有这些操作符:
= + - < > * / % | & ^ <= >= == != ? :
但是一元操作符后不要加空格:
& * + - ~ ! sizeof typeof alignof __attribute__ defined
后缀自加和自减一元操作符前不加空格:
++ --
前缀自加和自减一元操作符后不加空格:
++ --
.
和 ->
结构体成员操作符前后不加空格。
不要在行尾留空白。有些可以自动缩进的编辑器会在新行的行首加入适量的空白,然后 你就可以直接在那一行输入代码。不过假如你最后没有在那一行输入代码,有些编辑器 就不会移除已经加入的空白,就像你故意留下一个只有空白的行。包含行尾空白的行就 这样产生了。
Typedef
不要使用类似 vps_t
之类的东西。
对结构体和指针使用 typedef 是一个 错误 。当你在代码里看到:
vps_t a;
这代表什么意思呢?
相反,如果是这样
struct virtual_container *a;
你就知道 a
是什么了。
很多人认为 typedef 能提高可读性
。实际不是这样的。它们只在下列情况下有用:
完全不透明的对象 (这种情况下要主动使用 typedef 来 隐藏 这个对象实际上 是什么)。
例如:
pte_t
等不透明对象,你只能用合适的访问函数来访问它们。备注
不透明性和 “访问函数” 本身是不好的。我们使用 pte_t 等类型的原因在于真 的是完全没有任何共用的可访问信息。
清楚的整数类型,如此,这层抽象就可以 帮助 消除到底是
int
还是long
的混淆。u8/u16/u32 是完全没有问题的 typedef,不过它们更符合类别 (d) 而不是这里。
备注
要这样做,必须事出有因。如果某个变量是
unsigned long
,那么没有必要typedef unsigned long myflags_t;
不过如果有一个明确的原因,比如它在某种情况下可能会是一个
unsigned int
而在其他情况下可能为unsigned long
,那么就不要犹豫,请务必使用 typedef。当你使用 sparse 按字面的创建一个 新 类型来做类型检查的时候。
和标准 C99 类型相同的类型,在某些例外的情况下。
虽然让眼睛和脑筋来适应新的标准类型比如
uint32_t
不需要花很多时间,可 是有些人仍然拒绝使用它们。因此,Armino 特有的等同于标准类型的
u8/u16/u32/u64
类型和它们的有符号 类型是被允许的——尽管在你自己的新代码中,它们不是强制要求要使用的。当编辑已经使用了某个类型集的已有代码时,你应该遵循那些代码中已经做出的选 择。
可能还有其他的情况,不过基本的规则是 永远不要 使用 typedef,除非你可以明 确的应用上述某个规则中的一个。
总的来说,如果一个指针或者一个结构体里的元素可以合理的被直接访问到,那么它们 就不应该是一个 typedef。
函数
函数应该简短而漂亮,并且只完成一件事情。函数应该可以一屏或者两屏显示完,只做一 件事情,而且把它做好。
一个函数的最大长度是和该函数的复杂度和缩进级数成反比的。所以,如果你有一个理 论上很简单的只有一个很长 (但是简单) 的 case 语句的函数,而且你需要在每个 case 里做很多很小的事情,这样的函数尽管很长,但也是可以的。
不过,如果你有一个复杂的函数,而且你怀疑一个天分不是很高的高中一年级学生可能 甚至搞不清楚这个函数的目的,你应该严格遵守前面提到的长度限制。使用辅助函数, 并为之取个具描述性的名字 (如果你觉得它们的性能很重要的话,可以让编译器内联它 们,这样的效果往往会比你写一个复杂函数的效果要好。)
函数的另外一个衡量标准是本地变量的数量。此数量不应超过 5-10 个,否则你的函数 就有问题了。重新考虑一下你的函数,把它分拆成更小的函数。人的大脑一般可以轻松 的同时跟踪 7 个不同的事物,如果再增多的话,就会糊涂了。即便你聪颖过人,你也可 能会记不清你 2 个星期前做过的事情。
在源文件里,使用空行隔开不同的函数。
在函数原型中,包含函数名和它们的数据类型。
集中的函数退出途径
虽然被某些人声称已经过时,但是 goto 语句的等价物还是经常被编译器所使用,具体 形式是无条件跳转指令。
当一个函数从多个位置退出,并且需要做一些类似清理的常见操作时,goto 语句就很方 便了。如果并不需要清理操作,那么直接 return 即可。
选择一个能够说明 goto 行为或它为何存在的标签名。如果 goto 要释放 buffer
,
一个不错的名字可以是 out_free_buffer:
。别去使用像 err1:
和 err2:
这样的 GW_BASIC 名称,因为一旦你添加或删除了 (函数的) 退出路径,你就必须对它们
重新编号,这样会难以去检验正确性。
使用 goto 的理由是:
无条件语句容易理解和跟踪
嵌套程度减小
可以避免由于修改时忘记更新个别的退出点而导致错误
让编译器省去删除冗余代码的工作 ;)
int fun(int a)
{
int result = 0;
char *buffer;
buffer = malloc(SIZE);
if (!buffer)
return BK_ERR_NO_MEM;
if (condition1) {
while (loop1) {
...
}
result = 1;
goto out_free_buffer;
}
...
out_free_buffer:
free(buffer);
return result;
}
一个需要注意的常见错误是 一个 err 错误
,就像这样:
err:
free(foo->bar);
free(foo);
return ret;
这段代码的错误是,在某些退出路径上 foo
是 NULL。通常情况下,通过把它分离
成两个错误标签 err_free_bar:
和 err_free_foo:
来修复这个错误:
err_free_bar:
free(foo->bar);
err_free_foo:
free(foo);
return ret;
理想情况下,你应该模拟错误来测试所有退出路径。
注释
注释是好的,不过有过度注释的危险。永远不要在注释里解释你的代码是如何运作的: 更好的做法是让别人一看你的代码就可以明白,解释写的很差的代码是浪费时间。
一般的,你想要你的注释告诉别人你的代码做了什么,而不是怎么做的。也请你不要把 注释放在一个函数体内部:如果函数复杂到你需要独立的注释其中的一部分,你很可能 需要回到:ref:_function。你可以做一些小注释来注明或警告某些很聪明 (或者槽糕) 的 做法,但不要加太多。你应该做的,是把注释放在函数的头部,告诉人们它做了什么, 也可以加上它做这些事情的原因。
当注释内核 API 函数时,请使用请参考文档规范。
长 (多行) 注释的风格是:
/*
* This is the preferred style for multi-line
* comments in the Armino source code.
* Please use it consistently.
*
* Description: A column of asterisks on the left side,
* with beginning and ending almost-blank lines.
*/
注释数据也是很重要的,不管是基本类型还是衍生类型。为了方便实现这一点,每一行 应只声明一个数据 (不要使用逗号来一次声明多个数据)。这样你就有空间来为每个数据 写一段小注释来解释它们的用途了。
宏,枚举
用于定义常量的宏的名字及枚举里的标签需要大写。
#define CONSTANT 0x12345
在定义几个相关的常量时,最好用枚举。
宏的名字请用大写字母,不过形如函数的宏的名字可以用小写字母。
一般的,如果能写成内联函数就不要写成像函数的宏。
含有多个语句的宏应该被包含在一个 do-while 代码块里:
#define macrofun(a, b, c) \
do { \
if (a == 5) \
do_this(b, c); \
} while (0)
使用宏的时候应避免的事情:
影响控制流程的宏:
#define FOO(x) \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while (0)
非常 不好。它看起来像一个函数,不过却能导致 调用
它的函数退出;不要打
乱读者大脑里的语法分析器。
依赖于一个固定名字的本地变量的宏:
#define FOO(val) bar(index, val)
可能看起来像是个不错的东西,不过它非常容易把读代码的人搞糊涂,而且容易导致看起 来不相关的改动带来错误。
作为左值的带参数的宏: FOO(x) = y;如果有人把 FOO 变成一个内联函数的话,这 种用法就会出错了。
忘记了优先级:使用表达式定义常量的宏必须将表达式置于一对小括号之内。带参数 的宏也要注意此类问题。
#define CONSTANT 0x4000
#define CONSTEXP (CONSTANT | 3)
在宏里定义类似函数的本地变量时命名冲突:
#define FOO(x) \
({ \
typeof(x) ret; \
ret = calc_ret(x); \
(ret); \
})
ret 是本地变量的通用名字 - __foo_ret 更不容易与一个已存在的变量冲突。
分配内存
内存分配时,传递结构体大小的首选形式是这样的:
p = malloc(sizeof(*p), ...);
另外一种传递方式中,sizeof 的操作数是结构体的名字,这样会降低可读性,并且可能 会引入 bug。有可能指针变量类型被改变时,而对应的传递给内存分配函数的 sizeof 的结果不变。
强制转换一个 void 指针返回值是多余的。C 语言本身保证了从 void 指针到其他任何 指针类型的转换是没有问题的。
内联弊病
有一个常见的误解是 内联
是 gcc 提供的可以让代码运行更快的一个选项。虽然使
用内联函数有时候是恰当的,不过很多情况下不是这样。inline 的过度使用会使代码变大,
导致占用更多的指令高速缓存,从而使整个系统运行速度变慢。
一个基本的原则是如果一个函数有 3 行以上,就不要把它变成内联函数。这个原则的一 个例外是,如果你知道某个参数是一个编译时常量,而且因为这个常量你确定编译器在 编译时能优化掉你的函数的大部分代码,那仍然可以给它加上 inline 关键字。
人们经常主张给 static 的而且只用了一次的函数加上 inline,如此不会有任何损失, 因为没有什么好权衡的。虽然从技术上说这是正确的,但是实际上这种情况下即使不加 inline gcc 也可以自动使其内联。而且其他用户可能会要求移除 inline,由此而来的 争论会抵消 inline 自身的潜在价值,得不偿失。
条件编译
只要可能,就不要在 .c 文件里面使用预处理条件 (#if, #ifdef);这样做让代码更难 阅读并且更难去跟踪逻辑。替代方案是,在头文件中用预处理条件提供给那些 .c 文件 使用,再给 #else 提供一个空桩 (no-op stub) 版本,然后在 .c 文件内无条件地调用 那些 (定义在头文件内的) 函数。这样做,编译器会避免为桩函数 (stub) 的调用生成 任何代码,产生的结果是相同的,但逻辑将更加清晰。
最好倾向于编译整个函数,而不是函数的一部分或表达式的一部分。与其放一个 ifdef 在表达式内,不如分解出部分或全部表达式,放进一个单独的辅助函数,并应用预处理 条件到这个辅助函数内。
如果你有一个在特定配置中,可能变成未使用的函数或变量,编译器会警告它定义了但
未使用,把它标记为 __maybe_unused
而不是将它包含在一个预处理条件中。(然而,如
果一个函数或变量总是未使用,就直接删除它。)
在代码中,尽可能地使用 IS_ENABLED 宏来转化某个 Kconfig 标记为 C 的布尔 表达式,并在一般的 C 条件中使用它:
if (IS_ENABLED(CONFIG_SOMETHING)) {
...
}
编译器会做常量折叠,然后就像使用 #ifdef 那样去包含或排除代码块,所以这不会带 来任何运行时开销。然而,这种方法依旧允许 C 编译器查看块内的代码,并检查它的正 确性 (语法,类型,符号引用,等等)。因此,如果条件不满足,代码块内的引用符号就 不存在时,你还是必须去用 #ifdef。
在任何有意义的 #if 或 #ifdef 块的末尾 (超过几行的),在 #endif 同一行的后面写下 注解,注释这个条件表达式。例如:
#ifdef CONFIG_SOMETHING
...
#endif /* CONFIG_SOMETHING */