基于Linux Kernel的C编码风格

基于Linux Kernel的C编码风格

1. 换行

  • 每一行代码长度不得超过120个字符。

  • 任何一行超过120列宽度的语句都应该拆分成多个行,除非超过120列的部分可以提高可读性且不会隐藏信息。

  • 拆分出来的子句长度总是应该比其主句要短,并且应该对齐。这条法则同样适用于一个有很长的参数列表的函数头。

  • 示例:

    // 拆分前
    int ret = xxxx_init_func(arg1, xxxx_gen_arg2(1, 200), xxxx_gen_arg3(1, 200), xxxx_gen_arg4(1, 200), sizeof(int), sizeof(float));
    
    // 拆分后
    int ret = xxxx_init_func(arg1, xxxx_gen_arg2(1, 200),
                             xxxx_gen_arg3(1, 200),
                             xxxx_gen_arg4(1, 200),
                             sizeof(int), sizeof(float));
    

2. 缩进

  • 所有的缩进统一使用4个空格(除了makefile应该使用tab)。

  • 每一行代码不得超过三层的缩进,如果你写的代码需要超过三层的缩进,那么你应该考虑优化你的程序。

  • switch语句的缩进方式是让case与switch对齐:

    switch (suffix) {
    case 'G':
    case 'g':
        mem <<= 30;
        break;
    case 'M':
    case 'm':
        mem <<= 20;
        break;
    case 'K':
    case 'k':
        mem <<= 10;
        /* fall through */
    default:
        break;
    }
    
  • 不要在单独一行里写多个语句,除非你想干什么不为人知的事。

    if (condition) { do_this; }
        do_something_everytime;
    
  • 不要在行末留有空格。

  • 行末留有空格的代码(看起来相当的丑陋):

3. 括号

  • 非函数的语句块 (if, switch, for, while, do),把左括号放在行末,把右括号放在行首。

    if (x is true) {
        we do y
    }
    
    switch (action) {
    case KOBJ_ADD:
        return "add";
    case KOBJ_REMOVE:
        return "remove";
    case KOBJ_CHANGE:
        return "change";
    default:
        return NULL;
    }
    
  • 然而,有一个特殊的例子,就是函数:函数的左括号应该放在行首:

    int function(int x)
    {
        body of function
    }
    
  • 右括号一般是单独成一行的,除非右括号之后紧随着紧密结合的语句,例如 do-while 语句和 if 语句:

    do {
        body of do - loop
    } while (condition);
    
    if (x == y) {
        ..
    } else if (x > y) {
        ...
    } else {
        ....
    }
    
  • 在一个循环中超过一个语句的情况也同样需要使用括号:

    while (condition) {
        if (test) {
            do_something();
        }
    }
    

4. 空格

  • 在关键字之后(if, switch, case, for, do, while)添一个空格。

  • 在长得像函数的关键字(sizeof, typeof, alignof, attribute)后不添加空格,s = sizeof(struct file);

  • 不要在括号周围多此一举的添加空格,一个相当糟糕的例子,s = sizeof( struct file );

  • 在声明指针或者返回值为指针的函数时,星号的位置应该紧靠着变量名或函数名,而不是类型名,例如:

    char *linux_banner;
    unsigned long long memparse(char *ptr, char **retptr);
    char *match_strdup(substring_t *s);
    
  • 在二元运算符和三元运算符周围添加一个空格,例如:= + - < > * / % | & ^ <= >= == != ? :

  • 不要在一元操作符之后添加空格,例如:++ -- !

  • 不要在结构体成员操作符周围添加空格,. ->

  • 不要在行末添加多余的空格。

  • 如果一个表达式包含多个子表达式,子表达式之间用空格分开,子表达式自身不加空格。

    // 不包含子表达式的表达式
    a[i+1]
    // 包含多个子表达式的表达式
    a[i*step1*step2 + j*step2 + k]
    

5. 命名

  • C 是一种简洁粗旷的语言,因此,你的命名也应该是简洁的。C 程序员不会像 Modula-2 和 Pascal 程序员那样使用 ThisVariableIsATemporaryCounter 这种“可爱”的名字,一个 C 程序员会把这种变量命名为 tmp ,如此简洁易写。
  • 全局变量(只有当你真正需要的时候才用它)和全局函数需要使用描述性的名字。如果你有一个计算活跃用户数量的函数,你应该起这样一个名字 count_active_users() 或者类似的,而不是这样一个名字 cntusr()
  • 起一个包含类型的名字(匈牙利命名法)是摧残大脑的行为,编译器知道函数的类型并且会检查类型,这样的名字不会起到任何帮助,它仅仅会迷惑程序员。所以,也难怪微软做出了那么多充满了 bug 的程序。
  • 局部变量名应该简短,如果你需要写一个循环,定义一个计数器,在不产生歧义的情况下,你大可命名为 i ,命名为 loop_counter 是生产力很低的行为。同样地,tmp 可以是任何类型的临时变量。
  • 所有的函数命名以及变量命名都应该采用小写,在你觉得需要断开的地方使用 _ 断开,例如: module_init_api 而不是 moduleInitApi
  • 类名,结构体名,枚举名统一使用大驼峰命名法。

6. Typedefs

  • 所有的结构体以及枚举类型都应该使用 typedef 来定义,结构体应命名为结构体名字加上后缀 _t,枚举类型命名为枚举类型名加上后缀 _e,例如:

    typedef struct VirtualContainer {
        ...
    } virtual_container_t;
    // 不允许简写为 vc_t ,_t 前的名字必须与 struct 后的名字完全一致
    
    typedef enum CtrlCmd {
        ...
    } ctrl_cmd_e;
    // 不允许简写为 cc_t ,_e 前的名字必须与 enum 后的名字完全一致
    // 枚举变量前缀必须定义为 枚举类型名 ctrl_cmd 的大写 CTRL_CMD
    
  • 不要重复定义标准类型,例如 typedef unsigned int viz_uint 而应直接使用C标准头文件stdint.h

  • 对于三方库,以三方库中定义的类型为准。

7. 函数

  • 函数应该短小精悍,一个函数只干一件事。简单说就是,做一件事并且把它做好。
  • 然而,如果你的函数十分复杂,你怀疑一个不像你一样的天才是看不懂的,你应该遵守函数最大的长度的限制,使用一些有描述性名称的辅助函数。如果你认为函数的性能至关重要,你可以让编译器把这些辅助函数编译成内联函数,一般情况下编译器可以比你做得更好。
  • 另一个测量函数的因素是局部变量的数量,他们不应该超出5-10个这个范围,否则你就犯了一些错误。重新思考这个函数,把它拆分成更小的几段。人类的大脑一般只能同时关注七件不同的事,更多需要关注的事情意味着更多的困扰。尽管你认为你是个天才,但是希望你能够理解一段你两周之前写的代码。
  • 在源文件中,用一个空行分割不同的函数。
  • 函数原型中,参数名应该与参数类型一起写出来,尽管 C 语言允许只写上参数类型,但是更推荐参数名,因为这是一种为读者提供有价值信息的简单方式。
  • 源文件中确认不会被外部使用到的函数以及全局变量,都应加上static关键字,并在函数前加上一些前缀比如 __ 来修饰,而且应该在文件开头合适位置加以声明,这样在源文件很长时,也能够快速的跳转到函数定义的地方。
  • 对于不能被修改的指针,加上关键字const

8. 集中函数出口

  • 当函数有多个出口,并且返回之前需要做很多相似的工作时,比如清理空间,这时候 goto 语句是十分方便的。当然了,如果没有类似的清理工作要在返回之前做,那么直接返回即可。
  • 根据 goto 的作用来决定一个 label 的名字,如果 goto 语言要去释放缓存,那么out_free_buffer:会是一个好名字。避免使用 GW-BASIC 的命名方式,比如 err1: err2:,因为当你需要新加或者删除某些函数出口时,你就需要重新排列标签数字,这会让代码的正确性难以得到保证。
  • 使用 goto 的理由如下:
    • 无条件跳转易于理解和阅读。
    • 可以减少嵌套。
    • 可以减少修改个别函数出口代码所造成的错误。

9. 注释

  • 注释是好的,但是要避免过分注释。永远不要去尝试解释你的代码如何工作,而是花时间在写出好的代码来,解释一段烂代码是浪费时间。

  • 一般来说,你应该去说明你的代码做了什么,而不是怎么做。

  • 尽量避免在函数体内写注释,如果你的函数如此复杂,以致于你需要在函数体内分几段注释来解释,那么你应该回到第七节去看看。当然你可以写一小段的注释来标记或者提醒大家哪些地方写得真聪明(或者真烂),但是不要做得太过分。

  • 不要写错误的注释,错误的注释比没有注释更糟糕,也不要写任何无用的注释。

  • 多行注释推荐的格式如下:

    /*
     * This is the preferred style for multi-line
     * comments in the Linux kernel source code.
     * Please use it consistently.
     *
     * Description:  A column of asterisks on the left side,
     * with beginning and ending almost-blank lines.
     */
    

10. 数据结构

  • 对于多线程可见的一些数据结构,总是应该有引用计数。
  • 进行引用计数意味着你可以避免死锁,允许多个用户并行访问数据,并且不用担心数据因为睡眠或者其他原因而找不到。
  • 锁不是引用计数的替代品。锁是为了保持数据的一致性,而引用计数是一种内存管理计数。通常这两种技术都是需要的,不要把他们搞混。
  • 记住,如果其他线程可以发现并使用你的数据结构,而你却没有引用计数,那么这基本就是一个 bug。

11. 宏、枚举

  • 常量宏是大写的#define WIDTH 100 而不是 #define width 100

  • 枚举类型也是大写的,例如:

    typedef enum CtrlCmd {
        CTRL_CMD_LOCK,
        CTRL_CMD_UNLOCK,
    } ctrl_cmd_e;
    
    // 而不是:
    typedef enum CtrlCmd {
        ctrl_cmd_lock,
        ctrl_cmd_unlock,
    } ctrl_cmd_e;
    
  • 常量宏一般都使用大写,但是函数宏可以使用小写,包含多条语句的宏应该包含在一个 do-while 循环体中:

    #define macrofun(a, b, c)           \
        do {                            \
            if (a == 5)                 \
                do_this(b, c);          \
        } while (0)
    
  • 使用宏时应该避免的情况:

    • 重复发明宏,对于一些你可以使用的宏,你应该直接使用他们,而不是重新再定义一些新的宏。

    • 依赖局部变量的宏,#define FOO(val) bar(index, val)

    • 带参数的宏当作左值,FOO(x) = y;

    • 忘了加上括号,定义常量以及带有参数的宏,必须要括上括号。

    • 在定义宏函数时发生命名冲突,ret 是一个很容易和局部变量发生冲突的名字,而 __foo_ret 这样的名字则很少会发生冲突

      #define FOO(x)                      \
      ({                                  \
          typeof(x) ret;                  \
          ret = calc_ret(x);              \
          (ret);                          \
      })
      

12. 分配内存

  • 为一个结构体分配内存的形式最好是这样的: student_t *p = malloc(sizeof(*p));
  • 另一种写出结构体名字的方式(sizeof(struct name))会破坏可读性并且给 bug 制造了机会:修改结构体名字却忘了修改对于的 sizeof 语句。

13. 函数返回值与名称

  • 函数可以返回不同种类的值,但是最普遍的就是表示运行成功或失败的值。这样的值可以用预先定义好的错误码表示(-Exxx = failure, 0 = success),或者一个布尔值(0 = failure, non-zero = success)。
  • 混合两种方式会使代码变得复杂,并且很难找到 bug。为了避免这种问题,一定要谨记如下约定:
    • 如果函数名是一个短语,表示的是一个动作,或者一个命令,那么返回值应该使用错误码的方式。
    • 如果函数名是一句话,表示的是一个断言,那么应该使用布尔值的方式。
  • 例如,add work 是一个动作,那么 add_work() 返回值为 0 则表示成功,-EBUSY表示失败。list_is_empty 是一个断言,那么返回值为非0表示为空,0表示不为空。
  • 所有的函数都应该遵守这个约定。
  • 如果返回值是一些计算结果,那么当然不需要管这些东西。一般来说,计算结果出错了就表示失败了。典型的例子就是返回一个指针:使用 NULL 或者 ERR_PTR 来表示错误。

14. 条件编译

  • 无论在哪,不要在 .c 文件中使用条件编译命令(#if, #ifdef),这样干会导致代码可读性降低并且代码逻辑混乱。取而代之,应该在 .c 文件对应的头文件中使用这些条件编译,并且在每个 #else 分支注明对应的版本信息。

  • 把同一个版本的所有函数都写在一个 #ifdef 中,不要在其中写一部分,而又在外部写一部分。

  • 在 #endif 之后写上一个注释,注明这个 #ifdef 块对应的内容:

    #ifdef CONFIG_SOMETHING
    ...
    #endif /* CONFIG_SOMETHING */
    

15. 就近定义原则

  • 不被多个地方使用的宏以及局部变量的定义都应遵循就近原则。

    // 只有一个地方会使用的宏,就近定义
    #define foo_macro(x)  foo(x)
    
    int foo_func()
    {
        foo_macro(x);
    }
    
    // 错误示范
    int foo_func1()
    {
        int i = 0;
        ... // 50行或者更多代码后
        // 使用 i
        i = 1;
    }
    
    // 正确示范
    int foo_func1()
    {
        ... // 50行或者更多代码后
        int i = 0;
        // 使用 i
        i = 1;
    }
    

16. 头文件

  • 只在头文件中包含必要的的头文件,其他任何不必要的头文件都应包含在源文件中。

  • 系统库或者三方库使用符号<>引用。

  • 团队库使用符号""引用。

  • 在需要引用头文件的文件中,首先声明引用的系统库或者三方库,再声明引用的团队库。

    #include <stdio.h>
    #include <stdint.h>
    
    #include "app_log.h"
    #include "app_ver.h"
    
  • C头文件应加入以下宏来保证其函数始终以C的方式来编译:

    #ifdef __cplusplus
    extern "C" {
    #endif
    
    // 除了其他库头文件不应该放在这里,任何东西都应该放在这里
    
    #ifdef __cplusplus
    }
    #endif /* __cplusplus */
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

专注的罗哈哈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值