C 编程最好实践

zhiganglet Post at 2008/9/18 16:54:00
C 编程最好实践

  原文地址:[url]http://www-900.ibm.com/developerWorks/cn/linux/l-bppc/index.shtml[/url]

  [size=18:db26774567][b:db26774567]简介

  风格和指南

  其他

  结束语

  参考资料[/b:db26774567][/size:db26774567]

  Shiv Dutta([email protected] ),技术顾问,IBM

  Gary Hook([email protected]),高级技术顾问,IBM

  2003 年 9 月

  尽管 C 语言问世已近 30 年,但他的魅力仍未减退。C 语言继续吸引着众多的人们,他们为了编写新的应用程式,或移植或维护现有的应用程式而必须学习新技能。

  [size=18:db26774567][b:db26774567]简介[/b:db26774567][/size:db26774567]

  本文是为了满足研发人员的需要而写的。我们总结了一套指南,无论作为研发人员还是顾问,这些指南多年来一直都很好地指导着我们,我们把他们作为建议提供给您,希望对您的工作有所帮助。您也许不赞同其中的某些指南,但我们希望您会喜欢其中的一些并在您的编程或移植项目中使用他们。

  [size=18:db26774567][b:db26774567]风格和指南[/b:db26774567][/size:db26774567]

  •使用一种使代码具备可读性和一致性的源代码风格。假如没有团队代码风格或自己的风格,您能够使用和大多数 C 程式员采用的 Kernighan 和 Ritchie 风格相似的风格。然而,举一个极端的例子,有可能最终会写出和下面相似的代码:

  [code:1:db26774567]int i;main(){for(;i["]<i;++i){--i;}"];read('-'-'-',i+++"hell\

  o, world!\n",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i);[/code:1:db26774567]

  •— 1984 年模糊 C 代码大赛“差劲奖”。应代码作者需要匿名。

  •通常将主例程定义为 main()。对应的 ANSI 编写方式是 int main(void)(假如不考虑命令行参数的话)或 int main( int argc, char **argv 。ANSI 以前的编译器会省略 void 声明,或列出变量名连同紧随其后的声明。

  •空格

  充分利用水平和垂直空格。缩进和空格间距应反映出代码的块结构。

  应将条件运算符的长字符串分割成单独的几行。例如:

  [code:1:db26774567]if (foo->next==NULL && number < limit && limit <=SIZE

  && node_active(this_input)) {...[/code:1:db26774567]

  最好改成:

  [code:1:db26774567]if (foo->next == NULL

  && number < limit && limit <= SIZE

  && node_active(this_input))

  {

  ...[/code:1:db26774567]同样,应将描述得很周详的 for 循环分割成不同的行:

  [code:1:db26774567]for (curr = *varp, trail = varp;

  curr != NULL;

  trail = &(curr->next), curr = curr->next )

  {

  ...[/code:1:db26774567]对于其他复杂表达式(如使用三元运算符 ?: 的表达式),最好也将其分割成数行。

  [code:1:db26774567] z = (x == y)

  ? n + f(x)

  : f(y) - n;[/code:1:db26774567]

  •注释

  注释应描述正在发生什么事、如何完成他、参数表示什么、使用了哪些全局变量连同任何限制或错误。但要避免不必要的注释。假如代码比较清楚,并且使用了良好的变量名,那么他应该能够较好地说明自身。因为编译器不检查注释,所以不确保他们是正确的。和代码不一致的注释会起到相反的作用。过多的注释会使代码混乱。

  下面是一种多余的注释风格:

  [code:1:db26774567] i=i+1; /* Add one to i */[/code:1:db26774567]

  很明显变量 i 递增了 1。更有更糟的注释方法:

  [code:1:db26774567]/************************************

  * *

  * Add one to i *

  * *

  ************************************/

  i=i+1;[/code:1:db26774567]

  •命名约定

  具备前导和尾随下划线的名称是为系统用途而保留的,不应当用于任何用户创建的名称。约定规定:

  1. #define 常量应全部大写。

  2. enum 常量应以大写字母开头或全部大写。

  3. 函数、类型定义(typedef)和变量名连同结构(struct)、联合(union)和枚举(enum)标记名称应小写。

  为清楚起见,避免使用仅在大小写上有区别的名称,如 foo 和 Foo。同样,避免使用 foobar 和 foo_bar 这样的名称。避免使用看上去相似的名称。在许多终端和打印机上,“l”、“1”和“I”看上去很相似。使用名为“l”的变量很不明智,因为他看上去很象常量“1”。

  •变量名

  选择变量名时,长度不重要,但清楚的表达很重要。长名称可用于全局变量,因为他不常用,而将在每行循环上要使用的数组下标命名为 i 就完全够了。假如使用“index”或“elementnumber”的话,不但输入得更多,而且会使计算的细节不明确。假如使用长变量名,有时候会使代码更难理解。比较:

  [code:1:db26774567] for(i=0 to 100)

  array[i]=0[/code:1:db26774567]

  和

  [code:1:db26774567] for(elementnumber=0 to 100)

  array[elementnumber]=0;[/code:1:db26774567]

  •函数名

  函数名应反映函数执行什么操作连同返回什么内容。函数在表达式中使用,通常用于 if 子句,因此他们的意图应一目了然。例如:

  [code:1:db26774567] if (checksize(x))[/code:1:db26774567]

  没有帮助作用,因为他没有告诉我们 checksize 是在出错时返回 true 还是在不出错时返回 true;而

  [code:1:db26774567] if (validsize(x))[/code:1:db26774567]

  则使函数的意图很明确。

  •声明

  任何的外部数据声明前都应加上 extern 关键字。

  “指针”限定符“*”应紧邻变量名而不是类型。例如,应使用

  [code:1:db26774567] char *s, *t, *u;[/code:1:db26774567]

  而不是

  [code:1:db26774567] char* s, t, u;[/code:1:db26774567]

  后一条语句没有错,但可能不是我们期望的,因为没有将“t”和“u”声明为指针。

  •头文档

  头文档应按功能组织在一起,即,对单独子系统的声明应在单独的头文档中。此外,当代码从一个平台移植到另一个平台时有可能发生更改的声明应位于单独的头文档中。

  避免使用和库头文档名相同的专用头文档名。语句 #include "math.h" 假如在当前目录中很难找到所期望文档的话,会包括标准库 math 头文档。假如这是您期望的结果,能够注释掉这行 include 语句。

  最后说明一点,对头文档使用绝对路径名不是个好主意。C 编译器的“include-path”选项(在许多系统上为 -I — 大写的 i)是处理众多专用头文档库的最好选择方法;他允许在不改变源文档的情况下重新组织目录结构。

  •scanf

  在重要的应用程式中永远不要使用 scanf。他的错误检测不够完善。请看下面的示例:

  [code:1:db26774567] #include <stdio.h>

  int main(void)

  {

  int i;

  float f;

  printf("Enter an integer and a float: ");

  scanf("%d %f", &i, &f);

  printf("I read %d and %f\n", i, f);

  return 0;

  }[/code:1:db26774567]

  测试运行

  Enter an integer and a float: 182 52.38

  I read 182 and 52.380001

  另一个测试运行

  Enter an integer and a float: 6713247896 4.4

  I read -1876686696 and 4.400000

  •++ 和 --

  当对语句中的变量使用递增或递减运算符时,该变量不应在语句中出现一次以上,因为求值的顺序取决于编译器。编写代码时不要对顺序作假设,也不要编写在某一机器上能够如期运作但没有明确定义的行为的代码:

  [code:1:db26774567] int i = 0, a[5];

  a[i] = i++; /* assign to a[0]? or a[1]? */[/code:1:db26774567]

  •不要被表面现象迷惑

  请看以下示例:

  [code:1:db26774567] while (c == '\t' || c = ' ' || c == '\n')

  c = getc(f);[/code:1:db26774567]

  乍一看,while 子句中的语句似乎是有效的 C 代码。但是,使用赋值运算符而不是比较运算符却产生了语义上不正确的代码。= 的优先级在任何运算符中是最低的,因此将以下列方式解释该语句(为清楚起见添加了括号):

  [code:1:db26774567] while ((c == '\t' || c) = (' ' || c == '\n'))

  c = getc(f);[/code:1:db26774567]

  赋值运算符左边的子句是:

  [code:1:db26774567] (c == '\t' || c)[/code:1:db26774567]

  他不会产生左值。假如 c 包含制表符,则结果是“true”,并且不会执行进一步的求值,而“true”不能位于赋值表达式的左边。

  •意图要明确。

  当您编写的代码能够解释成另一种意图时,使用括号或用其他方法以确保您的意图清楚。假如您以后必须处理该程式的话,这有助于您理解您当初的意图。假如其他人要维护该代码,这能够让维护任务变得更简单。

  用能预见可能出现错误的方式编码,有时是可行的。例如,能够将常量放在比较等式的左边。即,不编写:

  [code:1:db26774567] while (c == '\t' || c == ' ' || c == '\n')

  c = getc(f);[/code:1:db26774567]

  而是编写:

  [code:1:db26774567] while ('\t' == c || ' ' == c || '\n' == c)

  c = getc(f);[/code:1:db26774567]

  用以下方法却会得到编译器诊断:

  [code:1:db26774567] while ('\t' = c || ' ' == c || '\n' == c)

  c = getc(f);[/code:1:db26774567]

  这种风格让编译器发现问题;上面的语句是无效的,因为他试图对“\t”赋值。

  •意想不到的麻烦。

  各种 C 实现通常在某些方面各有不同。坚持使用语言中可能对任何实现都是公共的部分会有帮助。通过这样做,您更容易将程式移植到新的机器或编译器,并且不大会碰到编译器特别性所带来的问题。例如,考虑字符串:

  [code:1:db26774567] /*/*/2*/**/1[/code:1:db26774567]

  这里利用了“最大适合(maximal munch)”规则。假如能够嵌套注释,则可将该字符串解释为:

  [code:1:db26774567] /* /* /2 */ * */ 1[/code:1:db26774567]

  两个 /* 符号和两个 */ 符号匹配,因此该字符串的值为 1。假如注释不嵌套,那么在有些系统上,注释中的 /* 就被忽略。在另一些系统上会针对 /* 发出警告。无论哪种情况,该表达式可解释为:

  [code:1:db26774567] /* / */ 2 * /* */ 1[/code:1:db26774567]

  2 * 1 求值得 2。

  •清空输出缓冲区

  当应用程式异常终止时,其输出的尾部常常会丢失。应用程式可能没有机会完全清空他的输出缓冲区。输出的某一部分可能仍在内存中,并且永远不会被写出。在有些系统上,这一输出可能有几页长。

  以这种方式丢失输出会使人误解,因为他给人的印象是程式在他实际失败很久之前就失败了。解决这一问题的方法是强制将输出从缓冲区清除,特别是在调试期间。确切的方法随系统的不同而有所不同,但是也有常用的方法,如下所示:

  [code:1:db26774567] setbuf(stdout, (char *) 0);[/code:1:db26774567]

  必须在将任何内容写到标准输出之前执行该语句。理想情况下,这将是主程式中的第一条语句。

  •getchar() — 宏还是函数

  以下程式将其输入复制到其输出:

  [code:1:db26774567] #include <stdio.h>

  int main(void)

  {

  register int a;

  while ((a = getchar()) != EOF)

  putchar(a);

  }[/code:1:db26774567]

  从该程式除去 #include 语句将使该程式无法编译,因为 EOF 将是未定义的。

  我们能够用以下方法重新编写该程式:

  [code:1:db26774567] #define EOF -1

  int main(void)

  {

  register int a;

  while ((a = getchar()) != EOF)

  putchar(a);

  }[/code:1:db26774567]

  这在许多系统上都可行,但在有些系统上运行要慢很多。

  因为函数调用通常要花较长时间,所以常常把 getchar 实现为宏。这个宏定义在 stdio.h 中,所以当除去 #include <stdio.h> 时,编译器就不知道 getchar 是什么。在有些系统上,假设 getchar 是返回一个 int 的函数。

  实际上,许多 C 实现在其库中都有 getchar 函数,部分原因是为了防止这样的失误。于是,在 #include < stdio.h> 遗漏的情况下,编译器使用 getchar 的函数版本。函数调用的开销使程式变慢。putchar 有同样的问题。

  •空指针

  空指针不指向任何对象。因此,为了赋值和比较以外的目的而使用空指针都是非法的。

  不要重新定义 NULL 符号。NULL 符号应始终是常量值零。任何给定类型的空指针总是等于常量零,而和值为零的变量或和某一非零常量的比较,其行为由实现定义。

  反引用 null 指针可能会导致奇怪的事情发生。

  •a+++++b 表示什么?

  解析他的唯一有意义的方法是:

  [code:1:db26774567] a ++ + ++ b[/code:1:db26774567]

  然而,“最大适合”规则需要将他分解为:

  [code:1:db26774567] a ++ ++ + b[/code:1:db26774567]

  这在语法上是无效的:他等于:

  [code:1:db26774567] ((a++)++) + b[/code:1:db26774567]

  但 a++ 的结果不是左值,因此作为 ++ 的操作数是不可接受的。于是,解析词法不明确性的规则使得以语法上有意义的方式解析该示例变得不可能。当然,谨慎的办法实际上是在不能完全确定他们的意义的情况下,避免这样的构造。当然,添加空格有助于编译器理解语句的意图,但(从代码维护的角度看)将这一构造分割成多行更可取:

  [code:1:db26774567] ++b;

  (a++) + b;[/code:1:db26774567]

  •小心处理函数

  函数是 C 中最常用的结构概念。他们应用于实现“自顶向下的”问题解决方法 — 即,将问题分解成越来越小的子问题,直到每个子问题都能够用代码表示。这对程式的模块化和文档记录有帮助。此外,由许多小函数组成的程式更易于调试。

  假如有一些函数参数还不是期望的类型,则将他们强制转换为期望的类型,即使您确信没有必要也应该这样做,因为(假如不转换的话)他们可能在您最意料不到的时候给您带来麻烦。换句话说,编译器通常将函数参数的类型提升和转换成期望的数据类型以符合函数参数的声明。但是,在代码中以手工方式这样做能够清楚地说明程式员的意图,并且在将代码移植到其他平台时能确保有正确的结果。

  假如头文档未能声明库函数的返回类型,那就自己声明他们。用 #ifdef/#endif 语句将您的声明括起来,以备代码被移植到另一个平台。

  函数原型应当用来使代码更健壮,使他运行得更快。

  •悬空 else

  除非知道自己在做什么,否则应避免“悬空 else”问题:

  [code:1:db26774567] if (a == 1)

  if (b == 2)

  printf("***\n");

  else

  printf("###\n");[/code:1:db26774567]

  规则是 else 附加至最近的 if。当有疑虑时,或有不明确的可能时,添加花括号以说明代码的块结构。

  •数组界限

  检查任何数组的数组界限,包括字符串,因为在您现在输入“fubar”的地方,有人可能会输入“floccinaucinihilipilification”。健壮的软件产品不应使用 gets()。

  C 下标以零作为开始的这一事实使任何的计数问题变得更简单。然而,掌控如何处理他们需要花些努力。

  •空语句

  for 或 while 循环的空语句体应当单独位于一行并加上注释,这样就表明这个空语句体是有意放置的,而不是遗漏了代码。

  [code:1:db26774567] while (*dest++ = *src++)

  ; /* VOID */[/code:1:db26774567]

  •测试真(true)还是假(false)

  不要以缺省方式测试非零值,即:

  [code:1:db26774567] if (f() != FAIL)[/code:1:db26774567]

  优于

  [code:1:db26774567] if (f())[/code:1:db26774567]

  尽管 FAIL 的值可能是 0(在 C 中视为假(false))。(当然,应当在这一风格和“函数名”一节中演示的构造之间作出权衡。)当以后有人认为失败的返回值应该是 -1 而不是 0 时,显式的测试对您会有帮助。

  常见的问题是使用 strcmp 函数测试字符串是否相等,决不应该以缺省方式处理他的结果。更可取的方法是定义宏 STREQ:

  [code:1:db26774567]#define STREQ(str1, str2) (strcmp((str1), (str2)) == 0)[/code:1:db26774567]

  用这种方法,语句

  [code:1:db26774567] If ( STREQ( inputstring, somestring ) ) ...[/code:1:db26774567]

  就具备隐含的行为,该行为不大会在您不知情的情况下改变(人们往往不会重新编写或重新定义象 strcmp() 这样的标准库函数)。

  不要用 1 检查相等性的布尔值(TRUE 和 YES 等);而要用 0 测试不等性(FALSE 和 NO 等)。绝大多数函数被确保在条件为假(false)时返回 0,但仅在条件为真(true)时才返回非零。因此,最好将

  [code:1:db26774567] if (func() == TRUE) {...[/code:1:db26774567]

  写成

  [code:1:db26774567] if (func() != FALSE)[/code:1:db26774567]

  •嵌入语句

  使用嵌入赋值语句要看时间和地点。在有些构造中,假如不使用更多且不易阅读的代码就没有更好的方法来实现结果:

  [code:1:db26774567] while ((c = getchar()) != EOF) {

  process the character

  }[/code:1:db26774567]

  使用嵌入赋值语句来提高运行时性能是可能的。但是,您应当在提高速度和降低可维护性之间加以权衡,在人为指定的位置使用嵌入赋值语句会导致可维护性降低。例如:

  [code:1:db26774567] x = y + z;

  d = x + r;[/code:1:db26774567]

  不应被替换为:

  [code:1:db26774567] d = (x = y + z) + r;[/code:1:db26774567]

  即使后者可能节省一个周期也不行。最终,这两者之间在运行时间上的差异将随着优化器的增强而减少,易维护性的差异却将增加。

  •goto 语句

  应保守地使用 goto。从数层 switch、for 和 while 嵌套中跳出来时,使用该语句很有效,但是,假如有这样的需要,则表明应将内部构造分解成单独的函数。

  [code:1:db26774567] for (...) {

  while (...) {

  ...

  if (wrong)

  goto error;

  }

  }

  ...

  error:

  print a message[/code:1:db26774567]

  当必须使用 goto 时,随附的标号应单独位于一行,并且同后续代码的左边相距一个制表符或位于一行的开头。对 goto 语句和目标都应加上注释,说明其作用和目的。

  •switch 中的“落空”(fall-through)

  当一块代码有数个标号时,将这些标号放在单独的行。这种风格和垂直空格的使用一致,并且使重新安排 case 选项(假如那是必需的话)成了一项简单的任务。应对 C switch 语句的“落空”特征加以注释,以便于以后的维护。假如这一特性曾给您带来“麻烦”,那么您就能够理解这样做的重要性!

  [code:1:db26774567] switch (expr) {

  case ABC:

  case DEF:

  statement;

  break;

  case UVW:

  statement; /*FALLTHROUGH*/

  case XYZ:

  statement;

  break;

  }[/code:1:db26774567]

  尽管从技术上说,最后一个 break 不是必需的,但是,假如以后要在最后一个 case 之后添加了另一个 case,那么一致地使用 break 能够防止“落空”错误。假如使用 default case 语句的话, 他应当永远是最后一个,并且(假如他是最后的语句)无需最后的 break 语句。

  •常量

  符号常量使代码更易于阅读。应尽量避免使用数字常量;使用 C 预处理器的 #define 函数给常量赋予一个有意义的名称。在一个位置(最好在头文档中)定义值还会使得管理大型程式变得更容易,因为只需更改定义就能够统一地更改常量值。能够考虑使用枚举数据类型作为对声明只取一组离散值的变量的改进方法。使用枚举还能够让编译器对您枚举类型的任何误用发出警告。任何直接编码的数字常量必须至少有一个说明值的出处的注释。

  常量的定义和他的使用应该一致;例如,将 540.0 用于浮点数,而不要通过隐式浮点类型强制转换使用 540。也就是说,在有些情况下,常量 0 和 1 能够以本身的形式直接出现,而不要以定义的形式出现。例如,假如某个 for 循环遍历一个数组,那么:

  [code:1:db26774567] for (i = 0; i < arraysub; i++)[/code:1:db26774567]

  很合理,而代码:

  [code:1:db26774567] gate_t *front_gate = opens(gate[i], 7);

  if (front_gate == 0)

  error("can't open %s\n", gate[i]);[/code:1:db26774567]

  就不合理。在第二个示例中,front_gate 是指针;当值是指针时,他应和 NULL 比较而不和 0 比较。即使象 1 或 0 这样的简单值,通常最好也使用象 TRUE 和 FALSE 这样的定义来表示(有时 YES 和 NO 读起来更清楚)。

  不要在需要离散值的地方使用浮点变量。这是由于浮点数不精确的表示决定的(请参阅以上 scanf 中的第二个测试)。使用 <= 或 >= 测试浮点数;精确比较(== 或 !=)也许不能检测出“可接受的”等同性。

  应将简单的字符常量定义为字符文字而不是数字。不提倡使用非文本字符,因为他们是不可移植的。假如必须使用非文本字符,尤其是在字符串中使用他们,则应使用三位八进制数(不是个字符)的转义字符(例如“\007”)来编写他们。即便如此,这样的用法应视为和机器相关,并且应按这一情况来处理。

  •条件编译

  条件编译可用于机器相关性、调试连同在编译时配置某些选项。能够用无法预料的方式轻易地组合各种控制。假如将 #ifdef 用于机器相关性,应确保当没有指定机器时会出错,而不是使用缺省的机器。#error 伪指令能够较方便地用于这一用途。假如使用 #ifdef 进行优化,缺省值应是未优化的代码而不是不可编译或不正确的程式。要确保对未优化的代码进行了测试。

  [size=18:db26774567][b:db26774567]其他[/b:db26774567][/size:db26774567]

  •象 Make 这样用于编译和链接的实用程式极大简化了将应用程式从一个环境移到另一个环境的任务。在研发期间,make 仅对那些自上次使用 make 以来发生了更改的模块进行重新编译。

  经常使用 lint。lint 是 C 程式检查器,他检查 C 源文档以检测并报告函数定义和调用之间类型的不匹配和不一致,连同可能存在的程式错误等。

  此外,研究一下编译器文档,了解那些使编译器变得“吹毛求疵”的开关。编译器的工作是力求精确,因此通过使用适当的命令行选项让他报告可能存在的错误。

  •使应用程式中全局符号的数量最少。这样做的好处之一是和系统定义的函数冲突的可能性降低。

  •许多程式在遗漏输入时会失败。对任何的程式都应进行空输入测试。这也可能帮助您理解程式的工作原理。

  •不要对您的用户或您所用的语言实现有任何过多的假设。那些“不可能发生”的事情有时的确会发生。健壮的程式能够防范这样的情形。假如需要找到某个边界条件,您的用户将以某种方式找到他!

  永远不要对给定类型的大小作任何假设,尤其是指针。

  当在表达式中使用 char 类型时,大多数实现将他们当作无符号类型,但有些实现把他们作为有符号的类型。当在算术表达式使用他们时,建议始终对他们进行类型强制转换。

  不要依靠对自动变量和 malloc 返回的内存进行的初始化。

  •使您程式的目的和结构清楚。

  •要记住,可能会在以后需要您或别的人修改您的代码或在别的机器上运行他。细心编写您的代码,以便能够将他移植到其他机器。

  [size=18:db26774567][b:db26774567]结束语[/b:db26774567][/size:db26774567]

  应用程式的维护要花去程式员的大量时间,这是众所周知的事。部分原因是由于在研发应用程式时,使用了不可移植和非标准的特性,连同不令人满意的编程风格。在本文中,我们介绍了一些指南,多年来他们一直给予我们很大帮助。我们相信,只要遵守这些指南,将能够使应用程式维护在团队环境中变得更容易。

  [size=18:db26774567][b:db26774567]参考资料[/b:db26774567][/size:db26774567]

  •Obfuscated C and Other Mysteries,由 Don Libes 编写,John Wiley and Sons, Inc. ISBN 0-471-57805-3

  •The C Programming Language,Second Edition,由 Brian W. Kernighan 和 Dennis M. Ritchie 撰写,Prentice-Hall,ISBN 0-13-110370-9

  •Safer C,由 Les Hatton 编写,McGraw-Hill,ISBN 0-07-707640-0

  •C Traps and Pitfalls 由 Andrew Koenig 编写,AT&T Bell Laboratories,ISBN 0-201-17928-9
已有 0 位网友发表了看法