编程精粹-----第1章 假想的编译程序
第1章 假想的编译程序
读者可以考虑一下倘若编译程序能够正确地指出代码中的所有问题,那相应程序的错误情况会怎样?这不单指语法错误,还包括程序中的任何问题,不管它有多么隐蔽。例如,假定程序中有“差1”错误,编译程序可以采用某种方法将其查出,并给出如下的错误信息
-> line 23: while (i<=j)
off by one error: this should be '<'
又如,编译程序可以发现算法中有下面的错误:
-> line 42: int itoa(int i, char* str)
algorithm error: itoa fails when i is -32768
再如,当出现了参数传递错误时,编译程序可以给出如下的错误信息:
-> line 318: strCopy = memcpy(malloc(length), str, length);
Invalid argument: memcpy fails when malloc returns NULL
好了,要求编译程序能够做到这一程度似乎有点过分。但如编译程序真能做到这些,可以想象编写无错程序会变得多么容易。那简直是小事一桩,和当前程序员的一般作法真没法比。
假如在间谍卫星上用摄像机对准某个典型的软件车间.就会看到程序员们正弓着身子趴在键盘上跟踪错误;旁边,测试者正在对刚作出的内部版本发起攻击,轮番轰炸式地输入人量的数据以求找出新的错误。你还会发现,测试员正在检查老版本的错误是否溜进了新版本。可以推想,这种查错方法比用上面的假想编译程序进行查错要花费大得多的工作量、确实如此,而且它还要有点运气。
运气?
是的,运气。测试者之所以能够发现错误,不正是因为他注意到了诸如某个数不对、某个功能没按所期望的方式工作或者程序瘫痪这些现象吗?再看看上面的假想编译程序给出的上述错误:程序虽然有了“差1”错误,但如果它仍能工作,那么测试者能看得出来吗?就算看得出来,那么另外两个错误呢?
这听起来好象很可怕但测试人员就是这样做的大量给程序输入数据,希望潜在的错误能够亮相。“噢,不!我们测试人员的工作可不这么简单,我们还要使用代码覆盖工具、自动的测试集、随机的“猴”程序、抽点打印或其他什么的”。也许是这样,但还是让我们来看看这些工具究竟做了些什么吧!覆盖分析工具能够指明程序中哪些部分未被测试到,测试人员可以使用这一信息派生出新的测试用例。至于其它的工具无非都是“输入数据、观察结果”这一策略的自动化。
请不要产生误解,我并不是说测试人员的所作所为都是错误的。我只是说利用黑箱方法所能做的只是往程序里填数据,并看它弹出什么。这就好比确定一个人是不是疯子一样。问一些问题,得到回答后进行判断。但这样还是不能确定此人是不是疯子。因为我们没法知道其头脑中在想些什么。你总会这样地问自己:“我问的问题够吗?我问的问题对吗……”。
因此,不要光依赖黑箱测试方法。还应该试着去模仿前面所讲的假想编译程序,来排除运气对程序测试的影响,自动地抓住错误的每个机会。
考虑一下所用的语言
你最后一次看推销字处理程序的广告是什么时候?如果那个广告是麦迪逊大街那伙人写的,它很可能是这么说:“无论是给孩子们的老师写便条还是为下期的《Great American Novel》撰稿,WordSmasher都能行,毫不费劲!WordSmasher配备了令人吃惊的233000字的拼写字典,足足比同类产品多51000个字。它可以方便地找出样稿中的打字错误。赶快到经销商那里去买一份拷贝。WordSmasher是从圆珠笔问世以来最革命性的书写工具!”。
用户经过不断地市场宣传熏陶,差不多都相信拼写字典越大越好,但事实并非如此。象em、abel和si这些词,在任何一本简装字典中都可以查到、但在me、able和is如此常见的情况下您还想让拼写检查程序认为em、abel和si也是拼写正确的词吗?如果是,那么当你看到我写的suing时,其本意很可能是与之风马牛不相及的using。问题不在于suing是不是一个真正的词而在于它在此处确实是个错误。
幸运的是,某些质量比较高的拼写检查程序允许用户删去象em这类容易引起麻烦的词。这样一来,拼写检查程序就可以把原来合法的单词看成是拼写错误。好的编译程序也应该能够这样 ─── 可以把屡次出错的合法的C习惯用法看成程序中的错误。例如,这类编译程序能够检查出以下while循环错放了一个分号:
/* 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表达式后由的分号肯定是个错误,但编译程序却认为这是一个完全合法的while语句,其循环体为空语句。由于有时需要空语句,有时不需要空语句,所以为了查出不需要的空语句,编译程序常常在遇到空语句时给出一条可选的警告信息,自动警告你可能出了上面的错误。当确定需要用空语句时,你就用。但最好用NULL使其明显可见。例如:
char* strcpy(char* pchTo, char* pchFrom)
{
char* pchStart = pchTo;
while(*pchTo++ = *pchFrom++)
NULL;
Return(pchStart);
}
由于NULL是个合法的C表达式,所以这个程序没有间题。使用NULL的更大好处在于编译程序不会为NULL语句生成任何的代码,因为NULL只是个常量。这样,编译程序接受显式的NULL语句,但把隐式空语句自动地当作错误标出。在程序中只允许使用一种形式的空语句,如同为了保持文字的一致性,文中只想使用zero的一种复数形式zeroes,因此要从拼写字典中删除另一种复数形式zeros。
另一个常见的问题是无意的赋值。C是一个非常灵活的语言,它允许在任何可以使用表达式的地方使用赋值语句。因此如果用户不够谨慎,这种多余的灵活性就会使你犯错误。例如,以下程序就出现了这种常见的错误:
if(ch = ‘’)
ExpandTab();
虽然很清楚该程序是要将ch与水平制表符作比较,但实际上却成了对ch的赋值。对于这种程序,编译程序当然不会产生错误,因为代码是合法的C。
某些编译程序允许用户在 && 和 表达式以及if、for和while构造的控制表达式中禁止使用简单赋值,这样就可以帮助用户查出这种错误。这种做法的基本依据是用户极有可能在以上五种情况下将等号==偶然地健入为赋值号=。
这种选择项并不妨碍用户作赋值,但是为了避免产生警告信息,用户必须再拿别的值,如零或空字符与赋值结果做显式的比较。因此,对于前面的strcpy例子,若循环写成:
while(*pchTo++ = *pchFrom++)
NULL;
编译程序会产生警告信息一所以要写成;
while( (*pchTo++ = *pchFrom++)!=’