【C 陷阱与缺陷】(八)建议

1. 不要说服自己相信“皇帝的新装”

有的错误极具伪装性和欺骗性。比如,第一章原来的例子是这样写的:

while (c == '\t' || c = ' ' || c == '\n')
c = getc(f) ;

如上,这个例子在 C 语言中是非法的。因为赋值运算符 = 的优先级比 while 子句中其他运算符的优先级都要低,因此上例可以这样解释:

while ( (c == '\t' || c) =  (' ' || c == '\n') )
c = getc(f) ;

当然,这是非法的:

(c == '\t' || c)不能出现在赋值运算的左侧。

2. 直截了当地表明意图

当你编写代码的本意是希望表达某个意思,但这些代码有可能被误解为另一种意思时,请使用括号或者其他方式让你的意图尽可能清楚明了。这样做不仅有助于你日后重读程序时能够更好地理解自己的用意,也方便了其他程序员日后维护你的代码。

有时候我们还应该预料哪些错误有可能出现,在代码的编写方式上做到事先预防,一旦错误真正发生能够马上捕获。例如,有的程序员把常量放在判断相等的比较表达式的左侧。换言之,不是按照习惯的写法:

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' 赋值,因而是非法的。

3. 考查最简单的特例

无论是构思程序的工作方式,还是测试程序的工作情况,这一原则都是适用的。当部分输入数据为空或者只有一个元素时,很多程序都会执行失败,其实这些情况应该是一早就应该考虑到的。这一原则还适用于程序的设计。在设计程序时,我们可以首先考虑一组输入数据全为空的情形,从最简单的特例获得启发。

4. 使用不对称边界

本系列第三章节关于如何表示取值范围的讨论,值得一读再读。C 语言中数组下标取值从 0 开始,各种计数错误的产生与这一点或多或少有关系。

我们一旦理解了这个事实,处理这些计数错误就变得不那么困难了。

5. 注意潜伏在暗处的Bug

各种C语言实现之间,都存在着或多或少的细微差别。我们应该坚持只使用C语言中众所周知的部分,而避免使用那些“生僻”的语言特性。这样做,我们能够很方便地将程序移植到一个新的机器或编译器,而且“遭遇”到编译器Bug的可能性也会大大降低。

6. 防御性编程

对程序用户和编译器的假设不要太多!

如果 C 编译器能够捕获到更多的编程错误,这当然不错。不幸的是,因为几方面的原因,要做到这一点很困难。最重要的原因也许是历史因素:长期以来,人们惯于用C语言来完成以前用汇编语言做的工作。因此,许多C程序中总有这样的部分,刻意去做那些严格说来在 C 语言所允许范围以外的工作。最明显的例子就是类似操作系统的东西。这样,一个C编译器要做到严格检测程序中的各种错误,就要对程序中本意是可移植的部分做到严格检测,同时对程序中那些需要完成与特定机器相关工作的部分网开一面。

另一个原因是,某些类型的错误从本质上说是难于检测的。考虑下面的函数:

void set(int *p, int n) {
*p = n;

这个函数是合法还是非法?离开一定的上下文,我们当然不可能知道答案。

如果像下面的代码一样调用这个函数:

int a[10];
set (a+537) ;

这当然是合法的,但如果这样来调用 set 函数:

int a[10] ;
set (a+1037) ;

上面的代码就是非法的了。ANSI C 标准允许程序得到数组尾端出界的第一个位置的地址,因此上面的后一个代码段从它本身来说并没有什么错误。C编译器要想捕获到这样的错误,就必须非常地“聪明”。

参考资料《C 缺陷与陷阱》