第6章风险事业假如将一程序员置于悬崖边,给他绳子和滑翔机,他会怎样从悬崖上下来呢?是沿绳子爬下来呢?还是乘滑翔机呢?还是干脆直接跳下来呢?是沿绳子爬下来还是使用滑翔机我们说不太准,但可以肯定,他不会跳下来,因为那太危险了。可是当程序员有几种可能的实现方案时,他们却经常只考虑空间和速度,而完全忽视了风险性。如果程序员处于这样的悬崖边而又忽视了风险性,只考虑选择到达崖底最有效的途径的话.结果又将如何呢?程序员忽视风险性,至少有两个原因:一是因为他们盲目地认为,不管他们怎样实现编码,都不会有错误。没有任何程序员会说:“我准备编写快速排序程序,并打算在程序中有三个错误。”程序员并没有打算出错,而后来错误出现了,他们也并不特别吃惊。我认为程序员忽视风险性的第二个原因也是主要原因:在于从来没有人教他们这样去问问题:“该设计有多大的风险性?该实现有多大的风险性?有没有更安全的方法来写这个表达式?能否测试一下该设计?”要想问出这些问题,首先必须从思想上放弃这样的观点:不管作出哪种选择,最后总能得到无错代码。即使该观点是正确的,可是什么时候能得到无错代码呢?是由于使用安全的编码,在几天或几周之后就可以得到无错代码呢?还是由于忽视了风险性,出现很多错误而需要经过数月的调试和修改之后才能得到无错代码呢?因此本章将讨论在某些普通的编码实践中所存在的一些风险,以及如何做才能减少甚至消除这些风险。long的位域有多长美国国家标准协会(ANSI)委员会查看了运行在众多平台上的各种C语言。他们发现:尽管人们认为C语言是可移植语言,但实际上并非如此。不仅不同系统的标准库不同,而且预处理程序和语言本身在许多重要方面也不相同。ANSI委员会对大多数问题进行了标准化,使程序员可以写出可移植的代码,但是,ANSI标准忽视了一个非常重要的方面,它没有定义象char、int和long这样一些内部数据类型。ANSI标准将这些重要的实现细节留给编译程序的研制者来决定,标准本身并没有具体定义这些类型。例如,某一个ANSI标准的编泽程序可能具有32位的int和char。它们在缺省状态下是有符号的;而另一个ANSI标准的编译程序可能有16位的int和char,缺省状态下是无符号的。尽管如此不同,然而,这两个编译程序可能都严格附合ANSI标准。请看下面的代码:charch;……ch=0xff;if(ch==0xff)……我的问题是if语句中的表达式求值为真还是为假呢?正确的回答是:不知道。因为这完全依赖于编译程序。如果在缺省时字符是无符号的,则表达式肯定为真。但对于字符为有符号的编译程序而言,如80x86和680x0的编译程序,则每次测试都会失败,这是由C语言的扩充规则决定的。在上面的代码中,字符ch与整型数0xff进行比较。根据C语言的扩充规则,编译程序必须首先将ch转换为整型int,两者类型一致后再进行比较。关键在于:如果ch是有符号的,则在转换中要进行符号位扩充,其值将从0xff扩充为0xffff(假设int是16位)。这就是测试失败的原因。上面是为证明作者观点而设计的例子。读者可能会说,那不是一段有实际意义的代码。但是,在下面的常用代码中也存在着同样的问题。char*pch;……if(*pch==0xff)……在该定义中,char类型不唯一,位域不正确。例如,以下位域的值域是多少?intreg:3;仍然是不知道。即使将reg定义为整型int,这就隐含了它是有符号的,但根据所使用的不同编译程序,reg既可以是有符号的,也可以是无符号的。如果要使reg明确地成为有符号的整型或无符号的整型,必须使用singnedint或unsignedint。short,int,long究竟有多大,ANSI标准没有给出。而将其留给编译程序的研制者来决定。ANSI委员会成员并非对错误定义数据类型的问题视而不见。实际上,他们考查了大量的C语言实现并得出结论:由于各编译程序之间的类型定义是如此之不同,以至于定义严格的标准将会使大量现存代码无效。而这恰恰违背了他们的一个指导原则:“现存代码是非常重要的”。他们的目的并不是要建立更好的语言,而是给现存的语言制定标准,只要有可能,他们就要保护现存的代码。对类型进行约束也将违背委员会的另外一个指导原则:“保持C语言的活力,即使不能保证它具有可移植性,也要使其速度快。”因此,如果实现者感到有符号字符对于给定的机器来说更有效、那么就使用有符号字符。同样,根据硬件实现者可以将int选择为16位、32位或别的位数、这就是说,在缺省状态下,用户并不知道是具有有符号的位域还是无符号的位域。内部类型在其规格说明中存在着一个不足之处,在今后升级或改变编译程序时、或移到新的目标环境时、或与其他单位共享代码时、甚至在改变工作并发现所用编译程序的规则全部改变时,这个不足就会体现出来。这并不意味着用户不能安全使用内部类型、只要用户不对ANSI标准没有明确说明的类型再作假设。用户就可以安全使用内部类型。例如,你可以用易变的char数据类型,只要它能提供0到127的值,这是有符号字符和无符号字符域的交集。所以,当代码写为:char*strcpy(char*pchTo,char*pchFrom){char*pchStart=pchTo;while((*pchTo++=*pchFrom++)!=’\0’)NULL;Return(pchStart);}时,它在任何编译程序上都可以工作,因为没有对域作假定。而以下代码就不可以:/*strcmp--比较两个字符串**如果strLeft<strRight,返回一个负值*如果strLeft==strRight,返回0*如果strLeftstrRight,返回一个正值*/intstrcmp(constchar*strLeft,constchar*strRight){for(NULL;*strLeft==*strRight;strLeft++,strRight++){if(strLeft==‘\0’)/*是否与最后的结束字符相匹配?*/return(0);}return((*strLeft*strRight)?-1:1);}这段代码,由于最后一行的比较操作而失去了可移植性。只要用户使用了“<”操作符或其它要用有符号信息的操作符,就迫使编译程序产生不可移植的代码。修改strcmp很容易,只须声明strLeft和strRight为无符号字符指针,或直接将其填在比较式中:(*(unsignedchar*)strLeft*(unsignedchar*)strRight)记住一个原则不要在表达式中使用“简单的”字符。由于位域也有同样的问题,因此也有一个类似的原则:任何时候都不要使用“简单的”位域。如果仔细阅读分析ANSI标准,就可以导出可移植类型集的定义。这些可移植类型可在多个编译程序上以多种数制工作。char0to127signedchar-l27to127(not-l28)unsignedchar0to255大小未定,但不小于8个字位short-32767to32767(not-32768)signedshort-32767to32767unsignedshort0to65535大小未定,但不小于16个字位long-2147483647to2147483647(not–2147483648)signedlong-2147483647to2147483647unsignedlong0to4294967295大小未定,但不小于32个字位inti:n0to2^(n-1)-1signedinti:n-(2^(n-1)-1)to2^(n-1)-1unsignedinti:n0to2^(n)-1大小未定,至少有n个字位可移植类型最值得注意之处是:它们只考虑了三种最通用的数制:壹的补码、贰的补码和有符号的数值。现在我们不必为写可移植代码担心了。处理该问题就象人们为自己厨房操作台挑选贴面瓷砖一样,大多数人都愿意挑选自己喜欢的,将来的房屋买主也能容忍的贴面瓷砖,这样到时候就不必为了卖房屋来拆除、更换贴面瓷砖了。读者也应以同样的方式来考虑可移植代码,在大多数情况下,写可移植性代码与写非可移植性代码一样容易。为了避免将来的重复劳动,最好写可移植代码。尽量用可移植的数据类型有些程序员可能认为使用可移植的类型比使用“自然的”类型效率低。例如,假定int类型对目标硬件其物理字长是最有效的。这就意味着这种“自然的”位数可能大于16位,所保持的值可能大于32767。现在假定用户的编译程序使用的是32位的int,且题目要求0至40,000的值域。那么,是考虑到机器可以在int内有效地处理40,000个值而使用int呢,还是坚持使用可移植类型,而用long代替int呢?答案是如果机器使用的是32位int.那么也可以使用32位long,这两者产生的代码即使不相同也很相似(事实证明是如此),因此要使用long。用户即便担心在将来必须支持的机器上使用long效率可能会低一些,也应该坚持使用可移植类型。使用有严格定义的数据类型数据上溢或下溢有这样一些代码,表面看起来很正确。但是由于实现上存在着微妙的问题,执行却失败了,这是最严重的错误。“简单字符”就是这种性质的错误。下面的代码也具有这样的错误,这段代码用作初始化标准tolower宏的查寻表。charchToLower[UCHAR_MAX+1];voidBuildToLowerTable(void)/*ASCII版本*/{unsignedcharch;/*首先将每个字符置为它自己*/for(ch=0;ch=UCHAR_MAX;ch++)chToLower[ch]=ch;/*现将小写字母放进大写字母的槽子里*/for(ch=‘A’;ch=‘Z’;ch++)chToLower[ch]=ch+’a’–‘A’;}……#definetolower(ch)(chToLower[(unsignedchar)(ch)])尽管代码看上去很可靠,实际上BuildToLowerTable很可能使系统挂起来。看一下第一个循环,什么时候ch大于UCHAR_MAX呢?如果你认为“从来也不会”,那就对了。如果你不这样认为,请看下面的解释。假设ch等于UCHAR_MAX,那么循环语句理应执行最后一次了。但是就在最后测试之前,ch增加为UCHAR_MAX+1,这将引起ch上溢为0。因此,ch将总是小于等于UCHAR_MAX,机器将进行无限的循环。通过查看代码,这个问题还不明显吗?变量也可能下溢,那将会造成同样的困境。下面是实现memchr函数的一段代码。它的功能是通过查寻存储块,来找到第一次出现的某个字符。如果在存储块中找到了该字符,则返问指向该字符的指针,否则,返回空指针。象上面的BuildToLowerTable一样,memchr的代码看上去似乎是正确的,实际上却是错误的。void*memchr(void*pv,unsignedcharch,size_tsize){unsignedchar*pch=(unsignedchar*)pv;while(--size=0){if(*pch==ch)return(pch);pch++;}return(NULL);}循环什么时候终止?只有当size小于0时,循环才会终止。可是size会小于0吗?不会,因为size是无符号值,当它为0时,表达式--size将使其下溢而成为类型size_t定义的最大无符号位。这种下溢错误比BuldToLowerTable中的错误更严重。假如,memchr在存储块中找到了字符,它将正确地工作,即使没有找到字符,它也不致使系统悬挂起来.而坚持查下去,直到在某处找到了这个字符并返回指向该字符的指针为止。然而,在某些应用中也可能产生非常严重的错误。我们希望编译程序能对“简单字符”错误和上面两种错误发出警告。但是几乎没有任何编译程序对这些问题给出警告。因此,在编译程序的销售商说有更好的编译代码生成器之前,程序员将依靠自已来发现上溢和下溢错误。但是,如果用户按照本书第4章的建议逐条跟踪代码,那么这三种错