代码风格

备注

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 能提高可读性 。实际不是这样的。它们只在下列情况下有用:

  1. 完全不透明的对象 (这种情况下要主动使用 typedef 来 隐藏 这个对象实际上 是什么)。

    例如: pte_t 等不透明对象,你只能用合适的访问函数来访问它们。

    备注

    不透明性和 “访问函数” 本身是不好的。我们使用 pte_t 等类型的原因在于真 的是完全没有任何共用的可访问信息。

  2. 清楚的整数类型,如此,这层抽象就可以 帮助 消除到底是 int 还是 long 的混淆。

    u8/u16/u32 是完全没有问题的 typedef,不过它们更符合类别 (d) 而不是这里。

    备注

    要这样做,必须事出有因。如果某个变量是 unsigned long ,那么没有必要

    typedef unsigned long myflags_t;

    不过如果有一个明确的原因,比如它在某种情况下可能会是一个 unsigned int 而在其他情况下可能为 unsigned long ,那么就不要犹豫,请务必使用 typedef。

  3. 当你使用 sparse 按字面的创建一个 类型来做类型检查的时候。

  4. 和标准 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)

使用宏的时候应避免的事情:

  1. 影响控制流程的宏:

#define FOO(x)                                  \
        do {                                    \
                if (blah(x) < 0)                \
                        return -EBUGGERED;      \
        } while (0)

非常 不好。它看起来像一个函数,不过却能导致 调用 它的函数退出;不要打 乱读者大脑里的语法分析器。

  1. 依赖于一个固定名字的本地变量的宏:

#define FOO(val) bar(index, val)

可能看起来像是个不错的东西,不过它非常容易把读代码的人搞糊涂,而且容易导致看起 来不相关的改动带来错误。

  1. 作为左值的带参数的宏: FOO(x) = y;如果有人把 FOO 变成一个内联函数的话,这 种用法就会出错了。

  2. 忘记了优先级:使用表达式定义常量的宏必须将表达式置于一对小括号之内。带参数 的宏也要注意此类问题。

#define CONSTANT 0x4000
#define CONSTEXP (CONSTANT | 3)
  1. 在宏里定义类似函数的本地变量时命名冲突:

#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 */