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] 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] 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] 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