1
读者可以考虑一下倘若编译程序能够正确地指出代码中的所有问题,那相应程序的错误
情况会怎样?这不单指语法错误,还包括程序中的任何问题,不管它有多么隐蔽。
2
假如在间谍卫星上用摄像机对准某个典型的软件车间。就会看到程序员们正弓着身子趴
在键盘上跟踪错误;旁边,测试者正在对刚作出的内部版本发起攻击轮,番轰炸式地输入人
量的数据以求找出新的错误。你还会发现,测试员正在检查老版本的错误是否溜进了新版本。
可以推想,这种查错方法比用上面的假想编译程序进行查错要花费大得多的工作量、确实如
此,而且它还要有点运气。
运气!
是的,运气。测试者之所以能够发现错误,不正是因为他注意到了诸如某个数不对、某
个功能没按所期望的方式工作或者程序瘫痪这些现象吗?再看看上面的假想编译程序给出
的上述错误:程序虽然有了“差1”错误,但如果它仍能工作,那么测试者能看得出来吗?
就算看得出来,那么另外两个错误呢?
3
请不要产生误解,我并不是说测试人员的所作所为都是错误的。我只是说利用黑箱方法
所能做的只是往程序里填数据,并看它弹出什么。这就好比确定一个人是不是疯子一样。问
一些问题,得到回答后进行判断。但这样还是不能确定此人是不是疯子。因为我们没法知道
其头脑中在想些什么。你总会这样地问自己:“我问的问题够吗?我问的问题对吗……?”
4
1)好的编译程序也应该能够这样 ─── 可以把屡次出错的合法的 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);
}
2)另一个常见的问题是无意的赋值。
if(ch = \t )
ExpandTab();
这种选择项并不妨碍用户作赋值,但是为了避免产生警告信息,用户必须再拿别的值,
如零或空字符与赋值结果做显式的比较。因此,对于前面的 strcpy 例子,若循环写成:
while(*pchTo++ = *pchFrom++)
NULL;
编译程序会产生警告信息一所以要写成
while( (*pchTo++ = *pchFrom++)!= \0 )
NULL;
这样做有两个好处。第一,现代的商用级编译程序不会为这种冗余的比较产生额外的代
码,可以将其优化掉。因此提供这种警告选择项的编译程序是可以信赖的。第二,它可以
少冒风险,尽管两种都合法,但这是更安全的用法。
3)另一类错误可以被归入“参数错误”之列。例如,多年以前,当我正在学 C语言时,
曾经这样调用过 fputc:
fprintf(stderr, Unable to open file %s. \n , filename);
fputc(stderr, \n );
这一程序看起来好象没有问题,但 fputc 的参数次序错了。
由于 ANSI C 标准要求每个库函数都必须有原型所以在 stdio.h头文件中能够找到 fputc 的原型。
fputc 的原型是
int fputc(int c, FILE* stream);
4)
ANSI C 虽然要求标准的库函数必须有原型,但并不要求用户编写的函数也必须有原型。
严格地说,它们可以有原型,也可以没有原型。如果用户想要检查出自己程序中的调用错误,
必须自己建立原型,并随时使其与相应的函数保持一致。
5)
Peter Lynch,据说是 80年代最好的合股投资公司管理者,他曾经说过:投资者与赌
徒之间的区别在于投资者利用每一次机会,无论它是多么小,去争取利益;而赌徒则只靠运
气。用户应该将这一概念同样应用于编程活动,选择编译程序的所有可选警告设施,并把这
些措施看成是一种无风险高偿还的程序投资。再不要问:“应该使用这一警告设施吗?而应
该问:“为什么不使用这一警告设施呢?”。要把所有的警告开关都打开,除非有极好的理由
才不这样做。
使用编译程序所有的可选警告设施。
5 增强原型的能力
不幸的是,如果函数有两个参数的类型相同,那么即使在调用该函数时互换了这两个参
数的位置,原型也查不出这一调用错误。例如,如果函数 memchr 的原型是:
void* memchr(const void*pv, int ch, int size);
那么在调用该函数时,即使互换其字符 ch 和大小 size参数,编译程序也不会发出警告信息。但是如果在相应界面和原型中使用了更加精确的类型,就可以增强原型提供的错误检查能力。例如如果有了下面的原型:
void* memchr(const void* pv, unsigned char ch, size_t size);
那么在调用该函数时弄反了其字符 ch 和大小 size参数,编译程序就会给出警告错误。
在原型中使用更精确类型的缺陷是常常必须进行参数的显式类型转换,以消除类型不匹
配的错误,即使参数的次序正确。
6 lint并不那么差
7 我做的修改很平常,我认为不会出错。
这些错误本来都应该不会进入原版源代码中,因为二者都可以几乎毫不费力地被查出
来。为什么程序员会犯这种错误呢?是他们过高地估计了自己编写正确代码的能力。
单元测试虽然意味着查错,但如果你根本就不进行单元测试也是枉然。
如果有单元测试,就进行单元测试。
转载请注明原创连接:http://blog.csdn.net/wujunokay/article/details/17722083