赋值运算符左边的子句是:
| (c == '\t' || c) |
它不会产生左值。如果 c 包含制表符,则结果是“true”,并且不会执行进一步的求值,而“true”不能位于赋值表达式的左边。
当您编写的代码可以解释成另一种意图时,使用括号或用其它方法以确保您的意图清楚。如果您以后必须处理该程序的话,这有助于您理解您当初的意图。如果其他人要维护该代码,这可以让维护任务变得更简单。
用能预见可能出现错误的方式编码,有时是可行的。例如,可以将常量放在比较等式的左边。即,不编写:
| while (c == '\t' || c == ' ' || c == '\n') c = getc(f); |
而是编写:
| while ('\t' == c || ' ' == c || '\n' == c) c = getc(f); |
用以下方法却会得到编译器诊断:
| while ('\t' = c || ' ' == c || '\n' == c) c = getc(f); |
这种风格让编译器发现问题;上面的语句是无效的,因为它试图对“\t”赋值。
各种 C 实现通常在某些方面各有不同。坚持使用语言中可能对所有实现都是公共的部分会有帮助。通过这样做,您更容易将程序移植到新的机器或编译器,并且不大会遇到编译器特殊性所带来的问题。例如,考虑字符串:
| /*/*/2*/**/1 |
这里利用了“最大适合(maximal munch)”规则。如果可以嵌套注释,则可将该字符串解释为:
| /* /* /2 */ * */ 1 |
两个 /* 符号与两个 */ 符号匹配,因此该字符串的值为 1。如果注释不嵌套,那么在有些系统上,注释中的 /* 就被忽略。在另一些系统上会针对 /* 发出警告。无论哪种情况,该表达式可解释为:
| /* / */ 2 * /* */ 1 |
2 * 1 求值得 2。
当应用程序异常终止时,其输出的尾部常常会丢失。应用程序可能没有机会完全清空它的输出缓冲区。输出的某一部分可能仍在内存中,并且永远不会被写出。在有些系统上,这一输出可能有几页长。
以这种方式丢失输出会使人误解,因为它给人的印象是程序在它实际失败很久之前就失败了。解决这一问题的方法是强制将输出从缓冲区清除,特别是在调试期间。确切的方法随系统的不同而有所不同,不过也有常用的方法,如下所示:
| setbuf(stdout, (char *) 0); |
必须在将任何内容写到标准输出之前执行该语句。理想情况下,这将是主程序中的第一条语句。
以下程序将其输入复制到其输出:
| #include <stdio.h> int main(void) { register int a; while ((a = getchar()) != EOF) putchar(a); } |
从该程序除去 #include 语句将使该程序无法编译,因为 EOF 将是未定义的。
我们可以用以下方法重新编写该程序:
| #define EOF -1 int main(void) { register int a; while ((a = getchar()) != EOF) putchar(a); } |
这在许多系统上都可行,但在有些系统上运行要慢很多。
因为函数调用通常要花较长时间,所以常常把 getchar实现为宏。这个宏定义在 stdio.h中,所以当除去 #include <stdio.h>时,编译器就不知道 getchar 是什么。在有些系统上,假设 getchar 是返回一个 int的函数。
实际上,许多 C 实现在其库中都有 getchar函数,部分原因是为了防止这样的失误。于是,在 #include <stdio.h>遗漏的情况下,编译器使用 getchar的函数版本。函数调用的开销使程序变慢。 putchar有同样的问题。
空指针不指向任何对象。因此,为了赋值和比较以外的目的而使用空指针都是非法的。
不要重新定义 NULL 符号。NULL 符号应始终是常量值零。任何给定类型的空指针总是等于常量零,而与值为零的变量或与某一非零常量的比较,其行为由实现定义。
反引用 null 指针可能会导致奇怪的事情发生。
解析它的唯一有意义的方法是:
| a ++ + ++ b |
然而,“最大适合”规则要求将它分解为:
| a ++ ++ + b |
这在语法上是无效的:它等于:
| ((a++)++) + b |
但 a++ 的结果不是 左值,因此作为 ++ 的操作数是不可接受的。于是,解析词法不明确性的规则使得以语法上有意义的方式解析该示例变得不可能。当然,谨慎的办法实际上是在不能完全确定它们的意义的情况下,避免这样的构造。当然,添加空格有助于编译器理解语句的意图,但(从代码维护的角度看)将这一构造分割成多行更可取:
| ++b; (a++) + b; |
函数是 C 中最常用的结构概念。它们应用于实现“自顶向下的”问题解决方法 — 即,将问题分解成越来越小的子问题,直到每个子问题都能够用代码表示。这对程序的模块化和文档记录有帮助。此外,由许多小函数组成的程序更易于调试。
如果有一些函数参数还不是期望的类型,则将它们强制转换为期望的类型,即使您确信没有必要也应该这样做,因为(如果不转换的话)它们可能在您最意料不到的时候给您带来麻烦。换句话说,编译器通常将函数参数的类型提升和转换成期望的数据类型以符合函数参数的声明。但是,在代码中以手工方式这样做可以清楚地说明程序员的意图,并且在将代码移植到其它平台时能确保有正确的结果。
如果头文件未能声明库函数的返回类型,那就自己声明它们。用 #ifdef/#endif 语句将您的声明括起来,以备代码被移植到另一个平台。
函数原型应当用来使代码更健壮,使它运行得更快。
除非知道自己在做什么,否则应避免“悬空 else”问题:
| if (a == 1) if (b == 2) printf("***\n"); else printf("###\n"); |
规则是 else 附加至最近的 if。当有疑虑时,或有不明确的可能时,添加花括号以说明代码的块结构。
检查所有数组的数组界限,包括字符串,因为在您现在输入“fubar”的地方,有人可能会输入“floccinaucinihilipilification”。健壮的软件产品不应使用 gets()。
C 下标以零作为开始的这一事实使所有的计数问题变得更简单。然而,掌握如何处理它们需要花些努力。
for 或 while 循环的空语句体应当单独位于一行并加上注释,这样就表明这个空语句体是有意放置的,而不是遗漏了代码。
| while (*dest++ = *src++) ; /* VOID */ |
不要以缺省方式测试非零值,即:
| if (f() != FAIL) |
优于
| if (f()) |
尽管 FAIL 的值可能是 0(在 C 中视为假(false))。(当然,应当在这一风格与“函数名”一节中演示的构造之间作出权衡。)当以后有人认为失败的返回值应该是 -1 而不是 0 时,显式的测试对您会有帮助。
常见的问题是使用 strcmp 函数测试字符串