📄 编程精粹.txt
字号:
他说:“不,你误解了。我的意思是你是否打算指出在将新做的修改并入原版源代码之前,程序员应该实际地进行相应的单元测试。我的小组中的一位程序员就是因为在进行了程序的修改之后没有进行相应的单元测试,使一个错误进入到我们的原版源代码中。”
这使我感到很惊奇。因为在Microsoft ,大多数项目负责人都要求程序员在合并修改了的源代码之前,要进行相应的单元测试。
“你没问他为什么不做单元测试吗”,我问道。
·6·
我的朋反从餐桌上抬起头来对我说:“他说他并没有编写任何新的代码,只是对现有代码进行了某些移动。他说他认为没必要再进行单元测试”。
这种事情在我的小组中也曾经发生过。
它使我想起曾经有一个程序员在进行了修改之后,甚至没有再编译一次就把相应的代码并入了原版源代码中。当然,我发现了这一问题,因为我在对原版源代码进行编译时产生了错误。当我问这个程序员怎么会漏掉这个编译错误,他说:“我做的修改很平常,我认为不会出错”,但他错了。
这些错误本来都应该不会进入原版源代码中,因为二者都可以几乎毫不费力地被查出来。为什么程序员会犯这种错误呢?是他们过高地估计了自己编写正确代码的能力。
有时,似乎可以跳过一些设计用来避免程序出错的步骤,但走捷径之时,就是麻烦将至之日。我怀疑会有许多的程序员甚至没有对相应的代码进行编译,就“完成”了某一功能。我知道这只是偶然情况,但绕过单元测试的趋势正在变强,尤其是作简单的改动。
如果你发现自己正打算绕过某个步骤,而它恰恰可以很容易地用来查错,那么一定要阻止自己绕过。相反,要利用所能得到的每个工具进行查错。此外,单元测试虽然意味着查错,但如果你根本就不进行单元测试也是枉然。
┏━━━━━━━━━━━━━━━━┓
┃如果有单元测试,就进行单元测试。┃
┗━━━━━━━━━━━━━━━━┛
小结
你认识哪个程序员宁愿花费时间去跟踪排错,而不是编写新的代码?我肯定有这种程序员,但至今我还没有见过一个。对于我认识的程序员,如果你答应他们再不用跟踪下一个错误,他们会宁愿一辈子放弃享用中国菜。
当你写程序时,要在心中时刻牢记着假想编译程序这一概念,这样就可以毫不费力或者只费很少的力气利用每个机会抓住错误。要考虑编译程序产生的错误,lint产生的错误以及单元测试失败的原因。虽然使用这些工具要涉及到很多的特殊技术,但如果不花这么多的功夫,那产品中会有多少个错误?
如果想要快速容易地发现错误,就要利用工具的相应特性对错误进行定位。错误定位得越早,就能够越早地投身于更有兴趣的工作。
要点:
●消除程序错误的最好方法是尽可能早,尽可能容易地发现错误,要寻求费力最小的自动查错方法。
●努力减少程序员查错所需的技巧。可以选择的编译程序或lint警告设施并不要求程序员要有什么查错的技巧。在另一个极端,高级的编码方法虽然可以查出或减少错误,但它们也要求程序要有较多的技巧,因为程序员必须学习这些高级的编码方法。
练习:
1) 假如使用了禁止在while的条件部分进行赋值的编译程序选择项,为什么可以查出下面代码中的运算优先级错误?
while(ch = getchar() != EOF)
·7·
2) 看看你怎样使用编译程序查出无意使用的空语句和赋值语句。值得推荐的办法是进行相应的选择,使编译程序能够对下列常见问题产生警告信息。怎样才能消除这些警告信息呢?
a) if(flight == 063) 这里程序员的本意是对63号航班进行测试,但因为前面多了一个0 使063 成了八进制数,结果变成对51号航班进行测试。
b) if(pb != NULL & *pb != 0xff) 这里不小心把&&键入为& ,结果即使pb等于NULL还会执行*pb != 0xff 。
c) quot = number/*pdenom; 这里无意间多了个* 号,结果使/*被解释为注释的开始。
d) word = bHigh<<8 + bLow; 由于出现了运算优先级错误,该语句被解释成了:
word = bHigh << (8+bLow);
3) 编译程序怎样才能对“没有与之配对的else”这一错误给出警告?用户怎样消除这一警告?
4) 再看一次下面的代码:
if(ch = '\t')
ExpandTab();
除禁止在if语句中使用简单赋值的方法之外,能够查出这个错误的另一种众所周知的方法是把赋值号两边的操作数颠倒过来:
if('\t' = ch)
ExpandTab();
这样,如果应该键人==时键入了= ,编译程序就会报错,因为不允许对常量进行赋值。这个办法彻底吗?为什么它不象编译程序开关自动化程度那么高?为什么新程序员会用赋值号代替等号?
5) C 的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX定义在limit.h 中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息地失败,因为预处理程序会把预定义的UINT_MAX替换成0 :
#if UINT_MAX>65535u
...
#endif
怎样使预处理程序报告出这一错误?
课题:
为了减轻维护原型的工作量,某些编译程序会在编译时自动地为所编译的程序生成原型。如果你用的编译程序没有提供这一选择项,自己写一个实用程序来完成这一工作。为什么标准的编码约定可以使这个实用程序的编写变得相对容易?
课题:
如果你用的编译程序还不支持本章(包括练习)中提及的警告设施,那么促进相应的制造商支持这些设施,另外要敦促他们除了允许用户设定或者取消对某些类错误的检查之外,还要提供有选择地设定或取消一些特定的警告设施。为什么要这样做呢?
·8·
第 2章 自己设计并使用断言
利用编译程序自动查错固然好,但我敢说只要你观察一下项目中那些比较明显的错误,就会发现编译程序所查出的只是其中的一小部分。我还敢说,如果排除掉了程序中的所有错误,那么在大部分时间内程序都会正确工作。
还记得第 1章中的下面代码吗?
strCopy = memcpy(malloc(length), str, length);
该语句在多数情况下都会工作得很好,除非malloc的调用产生失败。当malloc失败时,就会给memcpy返回一个NULL指针。由于memcpy处理不了NULL指针,所以,出现了错误。如果你很走运,在交付之前这个错误导致程序的瘫痪,从而暴露出来。但是如果你不走运,没有及时地发现这个错误,那某位顾客就一定会“走运”了。
编译程序查不出这种或其他类似的错误。同样,编译程序也查不出算法的错误,无法验证程序员所作的假定。或者更一般地,编译程序也查不出所传递的参数是否有效。
寻找这种错误非常艰苦,只有技术非常高的程序员或者测试者才能将它们根除并且不会引起其他的问题。
然而,假如你知道应该怎样去做的话,自动寻找这种错误就变得很容易了。
两个版本的故事
让我们直接进入memcpy,看看怎样才能查出上面的错误。最初的解决办法是使memcpy对NULL指针进行检查,如果指针为NULL,就给出一条错误信息,并中止memcpy的执行。下面是这种解法对应的程序:
/* memcpy—拷贝不重叠的内存块 */
void *memcpy1(void *pvTo, void *pvFrom, size_t size) {
byte *pbTo = (byte *)pvTo;
byte *pbFrom = (byte *)pvFrom;
if(pvTo == NULL || pvFrom == NULL) {
fprintf(stderr, "Bad args in memcpy\n");
abort();
}
while(size-- >0)
*pbTo++ = *pbFrom++;
return(pvTo);
}
·9·
只要调用时错用了NULL指针,这个函数就会查出来。所存在的唯一问题是其中的测试代码使整个函数的大小增加了一倍,并且降低了该函数的执行速度。如果说这是“越治病越糟”,确实有理,因为它一点不实用。要解决这个问题,需要利用C 的预处理程序。
如果保存两个版本怎么样?一个整洁快速,用于程序的交付;另一个臃肿缓慢(因为包括了额外的检查),用于调试,这样就得同时维护同一程序的两个版本,并利用C 的预处理程序有条件地包含或不包含相应的检查部分。
例如:只有当定义了DEBUG 才对相应的NULL指针测试部分进行编译怎么样?
void *memcpy(void *pvTo, void *pvFrom, size_t size) {
byte *pbTo = (byte *)pvTo;
byte *pbFrom = (byte *)pvFrom;
#ifdef DEBUG
if(pvTo == NULL || pvFrom == NULL) {
fprintf(stderr, "Bad args in memcpy\n");
abort();
}
#endif
while(size-- > 0)
*pbTo++ = *pbFrom++;
return(pvTo);
}
这种想法是同时维护调试和非调试(即交付)两个版本。在程序的编写过程中,编译其调试版本,利用它提供的测试部分在增加程序功能时自动地进行查错。在程序编完之后,编译其交付版本,封装之后交给经销商。
当然,你不会傻到直到交付的最后一刻才想到要运行打算交付的程序,但在整个的开发过程中,都应该使用程序的调试版本。正如在这一章和下一章所见,这样要求的主要原因是它可以显著地减少程序的开发时间。读者可以设想一下:如果程序中的每个函数都进行一些最低限度的错误检查,并对一些绝不应该出现的条件进行测试的话,相应的应用程序会有多么健壮。
这种方法的关键是要保证调试代码不在最终产品中出现。
┏━━━━━━━━━━━━━━━━━━━━━━━━┓
┃既要维护程序的交付版本,又要维护程序的调试版本。┃
┗━━━━━━━━━━━━━━━━━━━━━━━━┛
利用断言进行补救
说老实话,memcpy中的调试代码编得非常蹩脚,且颇有点喧宾夺主的意味。因此尽管它能产生很好的结果,多数程序员也不会容忍它的存在,这就是聪明的程序员决定将所有的调试代码隐藏在断言assert中的缘故。assert是个宏,它定义在头文件assert.h中,assert虽然不过是对前面所见#ifdef部分代码的替换,但利用这个宏,原来的代码从7 行变成了1 行。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -