linux下的c编程

时间:2023-11-10 14:03:14

                linux下的c编程

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

1.零长度数组

  GNUC 允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。

例如:

  struct var_data
  {
    int len;
    char data[0];
  };

  char data[0]仅仅意味着程序中通过 var_data 结构体实例的 data[index]成员可以访问 len 之后的第 index 个地址,它并没有为 data[]数组分配内存,因此 sizeof(struct var_data)=sizeof(int)。假设 struct var_data 的数据域保存在 struct var_data 紧接着的内存区域,通过如下代码可以遍历这些数据:

  struct var_data s;

  ...
  for (i = 0; i < s.len; i++)
  {
    printf("%02x", s.data[i]);
  }

2.case范围

  GNUC 支持 casex…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':

3.语句表达式

  GNU 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 中,对应的如下宏则会产生副作用:

  #define min(x,y) ((x) < (y) ? (x) : (y))
  代码 min(++ia,++ib)会被展开为((++ia) < (++ib) ? (++ia): (++ib)),传入宏的参数被增加两次

4.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 的类型是否一致。

5.可变参数的宏

  标准 C 只支持可变参数的函数, 意味着函数的参数是不固定的, 例如 printf()函数的原型为:

  int printf( const char *format [, argument]... );

  而在 GNUC 中,宏也可以接受可变数目的参数,例如:

  #define pr_debug(fmt,arg...) \
  printk(fmt,##arg)
  这里 arg 表示其余的参数可以是零个或多个,这些参数以及参数之间的逗号构成arg 的值,在宏扩展时替换 arg,例如下列代码:

  pr_debug("%s:%d",filename,line)
  会被扩展为:
  printk("%s:%d", filename, line)

  使用“##”的原因是理 arg 不代表任何参数的情况,这时候,前面的逗号就变得多余了。使用“##”之后,GNUC 预处理器会丢弃前面的逗号,这样,代码:

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

6.标号元素

  标准 C 要求数组或结构体的初始化值必须以固定的顺序出现,在 GNU C 中, 通过指定索引或结构体成员名,允许初始化值以任意顺序出现。

  指 定数 组 索 引的方 法是在 初 始 化 值 前 添加“ [INDEX]= ” , 当然也 可以 用“[FIRST … LAST]=” 的形式指定一个范围。 例如下面的代码定义一个数组,并把其中的所有元素赋值为 0:  unsigned char data[MAX] = { [0 ... MAX-1] 下面的代码借助结构体成员名初始化结构体:

  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,
    .ioctl = 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,
  };
7.当前函数名
  GNU C 预定义了两个标志符保存当前函数的名字, _ _FUNCTION_ _保存函数在源码中的名字,_ _PRETTY_FUNCTION_ _保存带语言特色的名字。在 C 函数中,这两个名字是相同的。

  void example()
  {
    printf("This is function:%s", _ _FUNCTION_ _);
  }
  代码中的_ _FUNCTION_ _意味着字符串“example” 。

8.特殊属性声明

  GNU C 允许声明函数、变量和类型的特殊属性, 以便进行手工的代码优化和定制代码检 查 的 方法。 指 定 一个声 明 的属性 , 只 需 要在声 明 后 添加__ a tt r i b u t e __ (( AT T R I B U T E )) 。其中 A TTRIBUTE 为属性说明,如果存在多个属性,则以逗号分隔。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 属性可以让编译器根据格式串检查参数类型。例如:

  asmlinkage int printk(const char * fmt, ...) _ _attribute_ _ ((format (printf, 1, 2)));

  上述代码中的第一个参数是格式串, 从第二个参数开始都会根据 printf()函数的格式串规则检查参数。

  unused 属性作用于函数和变量,表示该函数或变量可能不会被用到,这个属性可以避免编译器产生警告信息。

  aligned 属性用于变量、 结构体或联合体, 指定变量、 结构体或联合体的对界方式,以字节为单位,例如:

  struct example_struct

  {

    char a;

    int b;

    long c;

  } _ _attribute_ _((aligned(4)));

  表示该结构类型的变量以 4 字节对界。

  packed 属性作用于变量和类型, 用于变量或结构体成员时表示使用最小可能的对界,用于枚举、结构体或联合体类型时表示该类型使用最小的内存。例如:

  struct example_struct
  {
    char a;
    int b;
    long c _ _attribute_ _((packed));
  };

9.内建函数

  GNUC 提供了大量的内建函数, 其中大部分是标准 C 库函数的 GNUC 编译器内建版本,例如 memcpy()等,它们与对应的标准 C 库函数功能相同。不属于库函数的其他内建函数的命名通常以_ _builtin 开始,如下所示。

  1. 内建函数__builtin_return_address (LEVEL)返回当前函数或其调用者的返回地址,参数 LEVEL 指定调用栈的级数,如 0 表示当前函数的返回地址,1 表示当前函数的调用者的返回地址。
  2. 内建函数_ _builtin_constant_p(EXP)用于判断一个值是否为编译时常数, 如果参数 EXP 的值是常数,函数返回1,否则返回0。
  3. 内建函数_ _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)))
10.  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

展开的代码中存在两个问题:

(1)if 分支后有两个语句,导致 else 分支没有对应的 if,编译失败;

(2)假设没有 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)的使用完全是为了保证宏定义的使用者能无编译错误地使用宏,它不对其使用者做任何假设。

11.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 的用法简单而高效, 只需保证在错误处理时注销、 资源释放的顺序与正常的注册、释放申请的顺序相反。

本文总结于宋宝华老师的《linux设备驱动开发详解》P-73