嵌入式(驱动-基础):03---Linux下的C编程规范(documentation/coding、stylescripts/checkpatch.pl)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_41453285/article/details/102775929
  • Linux程序的命名习惯和Windows程序的命名习惯及著名的匈牙利命名法有很大的不同

一、documentation/coding文档

  • Linux有独特的编码风格,在Linux内核源代码下存在一个文件cocumentation/codingStyle,进行了比较详细的描述

二、stylescripts/checkpatch.pl脚本

  • 内核下的scripts/checkpatch.pl提供了1个检查代码风格的脚本

演示案例

//demo.c

for(int i=0;i<10;i++){
    a=c;
}

在SCM软件服务端部署checkpatch.pl脚本

  • 在工程阶段,一般可以在SCM软件的服务器端使能pre-commit hook,自动检查工程师提交的代码是否 符合Linux的编码风格,如果不符合,则自动拦截
  • git的pre-commit hook可以运行在本地代码仓库中,下面是一个演示版本
#!/bin/sh
#
# pre-commit hook to run check-patch on the output and stop any commits
# that do not pass. Note, only for git-commit, and not for any of the
# other scenarios
#
# Copyright 2010 Ben Dooks, <[email protected]>

if git rev-parse --verify HEAD 2>/dev/null >/dev/null
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
git diff --cached $against -- | ./scripts/checkpatch.pl --no-signoff

二、命名规范

  • 名称间使用下划线,使用小写字母

三、代码缩进

  • Linux的代码缩进使用“TAB”

四、空格使用

  • 另外,请注意代码中空格的应用,譬如“for *(i*=*0;*i*<*10;*i++)*{”   语 句中的“*”都是空格

五、Linux中代码括号“{”和“}”的使用原则

  • 1.对于结构体、if/for/while/switch语句, “{”不另起一行,例如:

  • 2.如果if、for循环后只有1行,不要加“{”和“}” ,例如:

  • 3.if和else混用的情况下,else语句不另起一行,例如:

  • 4.对于函数, “{”另起一行,譬如:

  • 5.在switch/case语句方面,Linux建议switch和case对齐,例如:

六、GNU C与ANSI C

  • Linux上可用的C编译器是GNU C编译器,它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布。GNU C对标准C进行一系列扩展,以增强标准C的功能

零长度和变量长度数组

  • GNU C允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。
  • 例如:char data[0]仅仅意味着程序中通过var_data结构体实例的data[index]成员可以访问len之后的第index个地 址,它并没有为data[]数组分配内存,因此sizeof(struct var_data)=sizeof(int)
struct var_data {
    int len;
    char data[0];
};
  • 假设struct var_data的数据域就保存在struct var_data紧接着的内存区域中,则通过如下代码可以遍历这 些数据:
struct var_data s;
...
for (i = 0; i < s.len; i++)
    printf("%02x",s.data[i]);

case范围

  • GNU C支持case x…y这样的语法,区间[x,y]中的数都会满足这个case的条件
  • 请看下面的代码:
switch (ch) {
=case '0'... '9': c -= '0';
    break;
case 'a'... 'f': c -= 'a' - 10;
    break;
case 'A'... 'F': c -= 'A' - 10;
    break;
}
  • 代码中的case'0'...'9'等价于标准C中的:
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':

语句表达式

  • NU C把包含在括号中的复合语句看成是一个表达式,称为语句表达式,它可以出现在任何允许表 达式的地方。我们可以在语句表达式中使用原本只能在复合语句中使用的循环、局部变量等
  • 例如:
#define min_t(type,x,y) \
({type _ _x =(x);type _ _y = (y); _ _x<_ _y _ _x: _ _y; })
int ia, ib, mini;
float fa, fb, minf;
mini = min_t(int, ia, ib);
minf = min_t(float, fa, fb);
  • 因为重新定义了__xx和__y这两个局部变量,所以用上述方式定义的宏将不会有副作用。在标准C中,对应的如下宏则会产生副作用:
    • 代码min(++ia,++ib)会展开为((++ia)<(++ib)(++ia):(++ib)),传入宏的“参数”增加 两次
#define min(x,y) ((x) < (y) (x) : (y))

typeof关键字

  • typeof(x)语句可以获得x的类型,因此,可以借助typeof重新定义min这个宏:
#define min(x,y) ({ \
const typeof(x) _x = (x); \
const typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x < _y _x : _y; })
  • 我们不需要像min_t(type,x,y)那个宏那样把type传入,因为通过typeof(x)、typeof(y)可以获 得type。代码行(void)(&_x==&_y)的作用是检查_x和_y的类型是否一致

可变参数宏

  • 标准C就支持可变参数函数,意味着函数的参数是不固定的
  • 例如printf()函数的原型为:
int printf( const char *format [, argument]... );
  • 而在GNU C中,宏也可以接受可变数目的参数,例如
#define pr_debug(fmt,arg...) \
            printk(fmt,##arg)
  • 这里arg表示其余的参数,可以有零个或多个参数,这些参数以及参数之间的逗号构成arg的值,在宏扩展时替换arg,如下列代码:
pr_debug("%s:%d",filename,line)

会被扩展为:
printk("%s:%d", filename, line)
  • 使用“##”是为了处理arg不代表任何参数的情况,这时候,前面的逗号就变得多余了。使用“##”之后, GNU C预处理器会丢弃前面的逗号,这样,下列代码:
pr_debug("success!\n")

会被正确地扩展为:
printk("success!\n")
而不是:
printk("success!\n",)

标号元素

  • 标准C要求数组或结构体的初始化值必须以固定的顺序出现,在GNU C中,通过指定索引或结构体成 员名,允许初始化值以任意顺序出现
  • 指定数组索引的方法是在初始化值前添加“[INDEX]=” ,当然也可以用“[FIRST...LAST]=”的形式指定 一个范围。例如,下面的代码定义了一个数组,并把其中的所有元素赋值为0:
unsigned char data[MAX] = { [0 ... MAX-1] = 0 };
  • 下面的代码借助结构体成员名初始化结构体:
struct file_operations ext2_file_operations = {
    llseek: generic_file_llseek,
    read: generic_file_read,
    write: generic_file_write,
    ioctl: ext2_ioctl,
    mmap: generic_file_mmap,
    open: generic_file_open,
    release: ext2_release_file,
    fsync: ext2_sync_file,
};
  • 但是,Linux 2.6推荐类似的代码应该尽量采用标准C的方式:
struct file_operations ext2_file_operations = {
    .llseek = generic_file_llseek,
    .read = generic_file_read,
    .write = generic_file_write,
    .aio_read = generic_file_aio_read,
    .aio_write = generic_file_aio_write,
    .ioct = ext2_ioctl,
    .mmap = generic_file_mmap,
    .open = generic_file_open,
    .release = ext2_release_file,
    .fsync = ext2_sync_file,
    .readv = generic_file_readv,
    .writev = generic_file_writev,
    .sendfile = generic_file_sendfile,
};

当前函数名

  • GNU C预定义了两个标识符保存当前函数的名字,__FUNCTION__保存函数在源码中的名字, __PRETTY_FUNCTION__保存带语言特色的名字。在C函数中,这两个名字是相同的
void example()
{
    printf("This is function:%s", __FUNCTION__);
}
  • 代码中的__FUNCTION__意味着字符串“example” 。C99已经支持__func__宏,因此建议在Linux编程中 不再使用__FUNCTION__,而转而使用__func__:
void example(void)
{
printf("This is function:%s", __func__);
}

特殊属性声明

  • GNU C允许声明函数、变量和类型的特殊属性,以便手动优化代码和定制代码检查的方法。要指定 一个声明的属性,只需要在声明后添加__attribute__((ATTRIBUTE))。其中ATTRIBUTE为属性说 明,如果存在多个属性,则以逗号分隔。
  • GNU C支持noreturn、format、section、aligned、packed等十多个 属性
  • noreturn属性作用于函数,表示该函数从不返回。这会让编译器优化代码,并消除不必要的警告信 息。例如
# define ATTRIB_NORET __attribute__((noreturn)) ....
asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;
  • format属性也用于函数,表示该函数使用printf、scanf或strftime风格的参数,指定format属性可以让编 译器根据格式串检查参数类型。例如:
    • 代码中的第1个参数是格式串,从第2个参数开始都会根据printf()函数的格式串规则检查参 数
asmlinkage int printk(const char * fmt, ...) __attribute__ ((format (printf, 1, 2)));
  • unused属性作用于函数和变量,表示该函数或变量可能不会用到,这个属性可以避免编译器产生警告信息
  • aligned属性用于变量、结构体或联合体,指定变量、结构体或联合体的对齐方式,以字节为单位,例如:表示该结构类型的变量以4字节对齐
struct example_struct {
    char a;
    int b;
    long c;
} __attribute__((aligned(4)));
  • packed属性作用于变量和类型,用于变量或结构体成员时表示使用最小可能的对齐,用于枚举、结构 体或联合体类型时表示该类型使用最小的内存。例如:
struct example_struct {
    char a;
    int b;
    long c __attribute__((packed));
};

内建函数

  • GNU C提供了大量内建函数,其中大部分是标准C库函数的GNU C编译器内建版本,例如 memcpy()等,它们与对应的标准C库函数功能相同
  • 不属于库函数的其他内建函数的命名通常以__builtin开始,如下所示:
    • 内建函数__builtin_return_address(LEVEL)返回当前函数或其调用者的返回地址,参数LEVEL指定 调用栈的级数,如0表示当前函数的返回地址,1表示当前函数的调用者的返回地址
    • 内建函数__builtin_constant_p(EXP)用于判断一个值是否为编译时常数,如果参数EXP的值是常 数,函数返回1,否则返回0
    • 内建函数__builtin_expect(EXP,C)用于为编译器提供分支预测信息,其返回值是整数表达式EXP 的值,C的值必须是编译时常数
  • 例如,下面的代码可检测第1个参数是否为编译时常数以确定采用参数版本还是非参数版本:
#define test_bit(nr,addr) \
(__builtin_constant_p(nr) \
constant_test_bit((nr),(addr)) : \
variable_test_bit((nr),(addr)))
  • Linux内核编程时常用的likely()和unlikely()底层调用的likely_notrace()、unlikely_notrace()就 是基于__builtin_expect(EXP,C)实现的
#define likely_notrace(x) __builtin_expect(!!(x), 1)
#define unlikely_notrace(x) __builtin_expect(!!(x), 0
  • 若代码中出现分支,则即可能中断流水线,我们可以通过likely()和unlikely()暗示分支容易成立还是不容易成立,例如:
if (likely(!IN_DEV_ROUTE_LOCALNET(in_dev)))
    if (ipv4_is_loopback(saddr))
        goto e_inval;
  • 在使用gcc编译C程序的时候,如果使用“-ansi–pedantic”编译选项,则会告诉编译器不使用GNU扩展语 法。例如对于如下C程序test.c:
struct var_data {
    int len;
    char data[0];
};
struct var_data a;

/********************/
直接编译可以通过:
gcc -c test.c
如果使用“-ansi–pedantic”编译选项,编译会报警:
gcc -ansi -pedantic -c test.c
test.c:3: warning: ISO C forbids zero-size array 'data'

七、do{}while(0)语句

  • 在Linux内核中,经常会看到do{}while(0)这样的语句,许多人开始都会疑惑,认为do{}while(0) 毫无意义,因为它只会执行一次,加不加do{}while(0)效果是完全一样的
  • 其实do{}while(0)的用法 主要用于宏定义中

演示案例

  • 这里用一个简单的宏来演示
#define SAFE_FREE(p) do{ free(p); p = NULL;} while(0)
  • 假设这里去掉do...while(0),即定义SAFE_DELETE为
#define SAFE_FREE(p) free(p); p = NULL;
  • 那么以下代码:
if(NULL != p)
    SAFE_DELETE(p)
else
    .../* do something */

会被展开为:
if(NULL != p)
    free(p); p = NULL;
else
    .../* do something */
  • 展开的代码中存在两个问题:
    • ①因为if分支后有两个语句,导致else分支没有对应的if,编译失败
    • ②假设没有else分支,则SAFE_FREE中的第二个语句无论if测试是否通过,都会执行
  • 的确,将SAFE_FREE的定义加上{}就可以解决上述问题了,即:
#define SAFE_FREE(p) { free(p); p = NULL;}
  • 这样,代码:
if(NULL != p)
    SAFE_DELETE(p)
else
    ... /* do something */

会被展开为:
if(NULL != p)
    { free(p); p = NULL; }
else
    ... /* do something */
  • 但是,在C程序中,在每个语句后面加分号是一种约定俗成的习惯,那么,如下代码:
if(NULL != p)
    SAFE_DELETE(p);
else
    ... /* do something */

将被扩展为:
if(NULL != p)
    { free(p); p = NULL; };
else
    ... /* do something */
  • 这样,else分支就又没有对应的if了,编译将无法通过。假设用了do{}while(0)语句,情况就不一样 了,同样的代码会被展开为:
if(NULL != p)
    do{ free(p); p = NULL;} while(0);
else
    ... /* do something */
  • 而不会再出现编译问题。do{}while(0)的使用完全是为了保证宏定义的使用者能无编译错误地使用宏, 它不对其使用者做任何假设。

八、goto语句

  • 用不用goto一直是一个著名的争议话题,Linux内核源代码中对goto的应用非常广泛,但是一般只限于 错误处理中
  • 例如:
if(register_a()!=0)
    goto err;
if(register_b()!=0)
    goto err1;
if(register_c()!=0)
    goto err2;
if(register_d()!=0)
    goto err3;
...

err3:
    unregister_c();
err2:
    unregister_b();
err1:
    unregister_a();
err:
    return ret;
  • 这种将goto用于错误处理的用法实在是简单而高效,只需保证在错误处理时注销、资源释放等,与正 常的注册、资源申请顺序相反

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/102775929