好长时间没更新BLOG了,向大家拜个晚年先。最近没怎么写代码,转几篇在网易虚拟社区发的文章过来充数。
Donald E. Knuth(高德纳)在TeX: The Program的前言中说:
"我相信,在1985年11月27日,TeX代码里面的最后一个BUG已经被发现和解决了。但是,如果代码中仍旧有BUG,我很高兴付给任何第一个发现BUG的人20.48美元(这是前一个金额的两倍,而且我计划在一年内把它翻倍。你看,我很自信!)"
想知道后来发生了什么吗?
在http://truetex.com/knuthchk.htm可以看到他写出去的支票的金额是从2.56美元开始翻倍的。微基百科中关于这种支票的文章(http://en.wikipedia.org/wiki/Knuth_reward_check)说,截至2001年10月为止,他写出去了超过两千张这样的支票,但是他的BUG支票是如此有名,以至于很多人把他的支票收藏起来而不是拿出去兑现(http://www.tug.org/whatis.html)。
有多少程序员在发布产品的时候可以这样自信地声明产品没有问题?
遗憾的是,现在的程序员经常把发现BUG的责任推给测试人员——“不用担心,测试人员会发现所有BUG的,这是他们的工作”。实际上,测试人员并没有开发人员的条件,他们不可能进行源代码级别的调试,很大程度上只能靠运气——没错,是靠运气,如果一个BUG很容易被发现,程序员不太可能自己没有发现它——来发现BUG。
还有一些人干脆就认为BUG是不可避免的,或者认为不值得这么精益求精(参见网易虚拟社区http://p5.club.163.com/viewArticleByWWW.m?boardId=clanguage&articleId=clanguage_108eacc622169e7&boardOffset=0的讨论),但是实际上防止BUG出现的最好的时机,就是在编写代码的时候。在编写代码一段时间之后,即使是编写者本人也可能需要一段时间来理解代码(如果不习惯写注释的话,这段时间会更长),更别说定位问题所在了。在编写代码时,如果具有良好的习惯,可以免去很多在之后消灭BUG的困难。
太多人把不要使用goto奉为圣旨,从来不想去打破。他们会争论,goto会造成难以维护的难读的代码,以及使编译器无法进行优化。这两点在很大程度上是真的,但是也有使用goto可以增加程序可读性和效率时候。在这种情况下,遵循“不使用goto语句”规范会产生更糟糕的代码。
一些人喜欢在成员函数后面加const,但是另外一些人没有养成这个习惯。一个直接的结果就是,一些看起来对对象完全没有影响的函数不能在const函数里面使用。这时候应该怎么办?看看Paul DiLascia建议的,把this指针强行转化为一个非const指针(http://www.microsoft.com/msj/archive/S126E.aspx)。如果函数实际上会对对象成员造成影响(例如CToolBar::GetItemRect),这也会带来潜在危险。
为了和ANSI标准之前编写的代码兼容,ANSI C中的memchr函数的声明为
void *memchr(
const void *buf,
int c,
size_t count
);
这里c是一个字符。很明显,标准为了兼容性放弃了明确性和更强的类型检查。如果放弃兼容性,这个函数应该声明为如下形式
void *memchr(
const void *buf,
unsigned char c,
size_t count
);
微软的很多代码使用一种叫做匈牙利表示法的命名规范。这使得标识符的含义和类型更加明确——但是这是从广义的角度来说的。考虑如下函数声明
char *strcpy(
char *strDestination,
const char *strSource
);
如果严格遵循原始的匈牙利表示法,那么两个参数的声明应该是pch开头。但是以str开头给这两个参数更多含义:它们指向以\0为结束符的字符串。
规范是用来在大部分时间里遵循,以及在可以得到更好的结果时打破的。
智能化的编译器开始将语法正确的语句列为警告:
while(size-->0);//注意这里有个分号
*pTo++=*pFrom++;
编译器会报告空循环问题。
但是对于以0结尾的字符串复制
while(*pTo++=*pFrom++);
,这样的警告是多余的。
更加常见的警告是在条件判断语句中
if(ch='\0')
EndOfString();
为了绕过这个警告,需要添加额外的运算或者语句,或者更正错误的赋值。
while((*pTo++=*pFrom++)!='\0'){}
if(ch=='\0')
一些程序员甚至将比较语句修改成
if('\0'==ch)
这样作的原因显而易见:为了减少潜在的BUG。如果你的编译器没有这样的警告,那么你可以使用一些工具来检查那些语法正确但是有潜在BUG的代码。LintProject (http://www.codeproject.com/tools/lintproject.asp)就是其中一个。但是,良好的编程习惯还是减少BUG出现的最好的方法。
在觉得警告消息太烦人的时候,不妨想想编译器的开发人员为什么要编写这么多警告消息,而不是仅仅寻求关闭警告的方法。
P.S. Visual C++的默认警告等级是3级。发布软件之前应该改成4级,之后检查所有的编译警告。
断言并不能抓住所有BUG——它们都是人写出来的,而是人就会犯错误。一些常见的错误包括:
这些并不是杞人忧天的问题——实际上,这些问题属于日常开发中最常见的问题。这些问题的特点是,它们并不是时常造成程序行为的异常,并且症状不可重复。以内存为例,释放内存之后编译器和操作系统通常不会自动去擦除内存中的内容,所以继续访问内存不太可能造成程序行为的异常——直到内存被重新分配出去,而另一块代码开始重写这块数据。申请资源之后丢失对资源的引用可能只是造成长时间运行之后系统资源不足而已。另外,这些问题都是算法的问题,而编译器并不会替你校验你的算法,你自己也不太可能会怀疑你自己的算法。
不要认为职业的程序员就不会犯这类错误。举几个例子来说明这些问题。
这些不确定的行为是很大一部分不可重复(因此也很难跟踪)的BUG的根源。举例来说,释放一块内存之后,在操作系统切换到另外一个线程之前,重新分配同一块内存,并且写入数据这个事件发生的可能性十分之小。为了解决这些问题,Visual C++编译器采取了一种保护性的措施,在调试模式下再分配和释放内存时将内存用一般不会用到的值填充,例如0xccccccc,0xdddddddd和0xfefefefe(参见编译器文档中的/RTC参数的说明)。这样你可以减少不可预料的程序行为,强迫BUG重现。如果你的编译器没有这么做,你可以自己编写一个调试模式下专用的内存管理程序进行这样的工作。为什么选择这样的值?在Intel系统中,0xcc的含义是int 3中断(参见http://blogs.msdn.com/oldnewthing/archive/2004/11/11/255800.aspx)——如果不小心执行了这块数据,那么程序会马上中断并且提示用户,其他的值则是典型的非法数据。如果你在为其他环境编写程序。你可能需要查阅一些资料来决定使用什么值来在调试模式下填充内存。
MFC的另一个保护措施是内存泄漏监测器——这也是在每个文件开头要加上#define new DEBUG_NEW的原因——但是这也变更了应用程序的行为。举例来说,为了检查内存泄漏,MFC总是分配比所需要多的内存,然后加入调试信息。如果你的程序有访问越界的代码,那么有可能擦除一部分额外分配的内存,可能的结果就是在调试模式下运行正常,而在发布模式下程序崩溃。当然,这是必须的。如果你的发布版本的程序依赖于这些额外字节,那么你就有麻烦了。
在发布模式下程序的崩溃有助于你发现问题,但是也造成定位问题的困难。你可以在发布模式下加入调试信息(没错,在工程的C++和连接选项中选中Program Database和Generate Debug Info)来生成一个中间版本;MSDN文章Generating and Deploying Debug Symbols with Microsoft Visual C++ 6.0(http://msdn.microsoft.com/library/en-us/dnvc60/html/gendepdebug.asp)甚至教你怎么怎么发布这样的版本,但是也要注意这样的版本和最终发布版本还是可能有区别的——特别是在程序中有BUG的情况下。另一个办法是在调试时将EIP寄存器修改成崩溃信息中的值,这样可以很容易在源代码中定位造成崩溃的代码的位置(参见http://www.codeproject.com/debug/XCrashReportPt1.asp)。
MFC开发中另一个比较有用的定位内存访问越界方法是,将数据封装成对象成员变量,尽量可能让所有类都从CObject派生,并且在代码中大量加上ASSERT_VALID。如果成员变量被越界的访问重写了,那么CObject指向AssertValid的虚函数表会被改写,而ASSERT_VALID会报告这个错误。
不要发布调试版本,这对用户来说并无意义。虽然这么说可能是多此一举,但是我在玩游戏的时候还真看见过断言对话框。调试信息是被设计用来发现问题的,不是用来隐藏问题的。如果你确实需要这么做(微软就定期发布核心模块的调试信息以供软件开发人员定位问题),那么你需要让用户认识到调试版本和最终发布版本的性能差异,例如在程序开始时显示一个消息。
广告时间:
可能大部分人已经知道了,但是CSDN论坛上仍旧不断出现关于这个兼容性的提问。最新的支持Visual C++ 6.0的版本是2003年2月版,下载地址是http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdk-full.htm。
取消对Visual C++ 6.0的支持的原因是为了支持新的/GS参数。XP SP2和Windows Server 2003 SP 1都增加了很多安全特性,以致于新的Windows SDK中包含的编译器和库文件不再和Visual C++ 6.0兼容。
参见