编程精粹--编写高质量C语言代码(1):假想编译程序
编译程序只能查找出程序的语法错误,而对于“数组越界訪问”,“对空指针解引用”等错误。编译程序是束手无策的。同一时候我们知道測试人员所使用的黑箱測试方法所能做的不过往程序里填数据,并看它弹出什么。这就决定了对程序错误的检測可能须要点运气。
假如编译程序可以检測出“数组越界訪问”,“差一错误”,“空指针”等等错误,那么编写无错代码事实上就要简答多了。
所以我们须要一个思维转变: 不要光依赖黑箱測试方法。还应该试着去模仿前面所讲的假想编译程序,来排除运气对程序測试的影响,自己主动地抓住错误的每一个机会。好的编译程序应该可以这样: 可以把屡次出错的合法的C习惯使用方法看成程序中的错误。这句话什么意思呢? 一些C使用方法从语法上讲是合法的,可是往往却给程序带来意想不到的错误。所以好的编译程序应该提供支持:让我们把这些使用方法当成错误。
举个样例:
/* memcpy 复制一个内存块 */ void* memcpy(void *pvTo,void *pvFrom, size_t size) { byte *pbTo=(byte *) pvTo; byte *pbFrom=(byte *)pvFrom; while(size-->0); *pbTo++=*pbFrom++; return pvTo; }
编译程序会让这个程序愉快地通过编译,可是程序执行后我们可能须要花费大量时间才干调试出这个很隐藏的错误:while条件推断语句之后多了一个分号。这导致循环体为空语句。这显然不是程序猿的意图。可是编译器却无法检測出这个错误。由于空语句从语法角度上是合法的。
虽然在C语言中空语句本身是合法的,可是我们的确非常少这样使用,出现空语句时往往是因为程序猿不小心导致的。而这样的空语句也会导致隐藏非常深的错误。所以当出现空语句时,假设编译器把它觉得是个错误。并自己主动给我们一个警告,这样让我们非常easy查找出错误。
当然假设我们的确要使用空语句时,那就用。
可是最好使用NULL使其明显可见。NULL仅仅是个常量,所以编译程序不会为NULL语句生成不论什么代码。
这样,编译程序仅仅接受显示的NULL语句,而把隐式的空语句(即仅仅有一个分号)标示为错误。
这就使得我们既能够明白地使用空语句,同一时候又能够指示出那些隐式的往往导致错误的空语句。
另一种常见的问题就是无意的赋值。比如
if(ch=‘\t') ExpandTab();
我们是想推断ch是否和‘\t’相等,却导致‘\t’赋值给ch。这导致程序的行为和我们期望的大相径庭。可是编译器却不会给出不论什么抱怨,由于这是合法的C语句。所以某些编译程序同意用户在&& 和 || 表达式以及if,for, while构造的表达式中禁止使用简单赋值。这样就能够帮助用户查出这样的错误。这样做的基本根据就是用户极有可能在以上五种情况下把“==”打成“=”。
可是有时为了代码的简单性,我们可能编写出下面代码
while(*pchTo++=*pchFrom++) NULL;
所以此时为了避免警告信息,我们能够这样编写
while((*pchTp++=*pchFrom)!='\0') NULL;
这样做虽然看上去要麻烦。可是现代的商业级编译器不会为这样的的冗余代码产生额外的代码,会把它优化掉。同一时候又能够降低风险。更加安全。
空语句。错误的赋值以及原型检查等仅仅是很多C编译程序提供的选择项中的一小部分内容。实际上还有很多其它的选择项。这里的要点是:用户能够选择的编译程序警告设施能够就可能的错误向用户发出警告信息。虽然有时为了这些警告设施,我们可能须要一些额外的工作,可是我们应该把这些警告设施看成一种无风险高偿还的程序投资。
使用编译程序全部的可选警告设施。
还有一种检查错误更具体。更彻底的方法是使用lint。lint这个工具最初是用来扫描C源文件并对源程序中不可移植的部分提出警告,如今的lint有用程序变得更加严谨,lint能够检測出尽管可移植而且全然合乎语法但非常有可能是错误的特性。
使用lint来检查出编译程序漏掉的错误。
有时,似乎能够跳过一些设计用来避免程序出错的步骤,比如单元測试。可是走捷径之时。就是麻烦将至之日。
假设有单元測试,就进行单元測试。
总结: 当你敲代码时,要在心中时刻牢记着假想编译程序这一概念,这样就能够花费非常少力气利用每一个机会抓住错误。
要考虑编译程序产生的错误。lint产生的错误以及单元測试失败的原因。消除程序错误的最好方法是尽可能早。尽可能easy地发现错误。要寻求费力最小的自己主动差错方法。
最后用作者在本章里的一句引言结束这篇文章:
投资者与赌徒之间的差别在于投资者利用每一次机会,不管它是多么小。去争取利益。而赌徒仅仅靠运气。