一、诊断错误及其处理
防止程序出错的第一关是编译器。如果遇到约束违规的情况或语法错误,编译器至少会生成一个诊断错误信息。大多数编译器将其诊断信息分为两类:错误和警告。语法错误很常见,也较易修改。调试程序时首先要改正的是语法错误,调试本身是一种艺术,是程序中断时试着去修复的一种艺术。
糟糕的语法会使编译器混乱,甚至可能达到生成很多错误的程度,程序设计者对这种情况不要大惊小怪,应冷静对待,其实很可能仅仅是由于某一个语句引起的,比如说漏掉了while循环中的一个大括号或语句末尾键入了冒号而非分号等。为此我们应该养成一种习惯,自顶向下的修改方法,每次从错误表的开头开始,一次修改一、二个错误再编译,或许错误就能减少许多甚至全部语法错误。
模块化程序设计也有助于程序的排错。调试一个充满连接和转折、全局变量等的程序要比调试一个精心设计的模块化程序困难得多。如果程序分成几个模块,各个模块负责程序的一个专项功能,一旦识别出问题可能所处的模块,就很容易地通过检查源代码方式来发现错误,这种策略也称为分治法,因为如果知道了没有问题之处,几乎等同于知道了问题发生的地方。
有时我们也可以忽略警告,但这并非是一种良好的编程习惯,追踪每个警告的原因并认真考虑是否有更稳健的方法编写代码能够帮助编程者编写更好的代码。如果我们只是忽略它,当这些“无害”的警告不断累积到一定程度时,可能面临出现混乱的危险。
二、差1错误及其处理
在C语言中,一个拥有n个元素的数组,不存在下标为n的元素,其元素下标的允许取值X围为0到n-1。请考察下列一段代码:
int a[10],i; for(i=1; i<=10; i++) a[i]=0;
这段代码本意是要设置数组a中的10个元素均为0,却产生了一个出人意料的“副作用”。循环把并不存在的a[10]元素设置为0。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,则内存中数组a之后的2个字节实际上分配给了整型变量i。此时,本来循环计数器i的值为10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0,这就陷入了死循环。
这是程序设计中较常见也较难觉察的一类错误,被称为“差一错误”(off-by-one error)。“差一错误”也称“栏杆错误”,问题说的是:100英尺长的围栏每隔10英尺需要一根支撑用的栏杆,一共需要多少根呢?如果不假思索,将100除以10得到10,当然这个答案是错误的,正确的答案应该是11。是否存在一些编程技巧,能够降低这类错误发生的可能性呢?“不对称边界”法给程序设计带来的便利非常明显。在“不对称边界”法中,可以规定数组元素的第一个“入界点”(对C语言而言,0就是数组下标的第一个“入界点”)和第一个“出界点”(对上述定义的数组而言,第一个出界点就是10,它不在数组下标X围之内)。在这种方法下,“出界点”的值即是数组的长度。
三、“悬挂”else错误及其处理
这个问题并非C语言所独有,也已经为人熟知,但即使是有经验的C程序员,也常常在此失误。如果想实现当x 分别是大于0、等于0和小于0时y值分别取得1、0和-1。考虑下面的程序片段:
y=0;
if(x>=0)
if(x>0)  y=1;
else y=-1;
然而,这段代码实际上所做的与编程者的愿望相去甚远。原因在于C语言中有这样的规则,else总是与同一对括号内最近的未匹配的if结合。如果我们按照上面这段程序实际上被执行的逻辑来调整代码缩进,大致是这样:y=0;
if(x>=0)
if(x>0)  y=1;
else y=-1;
也就是说,并非是当x<0时y=-1,而是当x=0时也使y=-1。
解决这一问题我们可以用“封装”的办法,将上述程序改为:
y=0;
if(x>=0)
{if(x>0)  y=1;}
else y=-1;
现在,else与第一个if结合,即使它离第二个if更近也是如此,因为此时第二个if已经被括号“封闭”起来了。
四、整数溢出及其处理
C语言为编程者提供了三种不同长度的整数:short int、int和long int,但不管是哪种类型表示的整数总有一定的X围,越出该X围时称为整数的溢出。例如现有算法要求如下:求满足条件1+2+3+…+n≤32767
的最大整数n,请考察如下程序段:
int n=1,sum=0;
while(sum<=32767) {sum+=n; n++;}
printf(“n=%d\n”,n-1);
乍看该程序时无错误,但事实上,上列程序中的while循环是一个无限循环,原因在于int型数的表示X围为-32768到+32767,当累加和sum超过32767时,便向高位进位,而对int型数而言,最高位表示符号,故sum超过32767后便得到一个负数,while条件当然满足,从而形成无限循环。此时,最好的解决办法是将sum定义为long int型。
五、词法陷井及其处理
(1)=不同于==
这是初学者最易犯的一个错误,符号=作为赋值运算符,符号==作为比较。一般而言,赋值运算相对于比较运算出现得更频繁,因此,字符数少的符号=就被赋予了更常用的含义――赋值运算。此外,在C语言中赋值符号被作为一种操作符对待,因而重复进行赋值运算(如a=b=c=5)可很容易地书写,并
且赋值操作还可以嵌入到更大的表达式中。这种使用上的便利性可能导致一个潜在的问题:本意是作比较运算时,却可能无意中误写成了赋值运算。该错误大多数情况下可以通过简单的要素项重排而防止。从编译器的角度出发,对于相等测试,变元在等号的哪一边无关紧要,如果两边都是变量,则需要留意符号“=”的个数。但是,如果一边是常量,则存在可以防止错误的适当措施。我们何不养成把常量放在比较运算符左边的习惯呢?因为这样一来,即便漏掉了一个“=”符号,保证会出现一个编译错误,因为不能给常数赋值。
(2)字符与字符串
C语言中的单引号和双引号含义迥异,用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值,因此,采用ASCII 字符集的编译器而言,‘a’的含义与0141或97严格一致。而用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制值为零的字符‘\0’初始化。
整型数(一般为16位或32位)的存储空间中可以容纳多个字符(一般为8位),因此,有的C编译器允许在一个字符常量(以及字符串常量)中包含多个字符。也就是说,用‘yes’代替“yes”不会被编译器检测到,后者的含义是“依次包含‘y’、‘e’、‘s’以及字符‘\0’的4个连续内存单元的首地址”,而前者的含义并没有正确地定义,有些C编译器会处理成出错,但大多数C编译器的理解为“一个整数值,由‘y’、‘e’、‘s’所代
表的整数值按照特定编译器实现中定义的方式组合得到”。因此,这两者如果在数值上有什么相似之处,也完全是一种巧合而已。
(3)整数溢出
C语言为编程者提供了三种不同长度的整数:short int、int和long int,但不管是哪种类型表示的整数总有一定的X围,越出该X围时称为整数的溢出。例如现有算法要求如下:求满足条件1+2+3+…+n≤32767的最大整数n,请考察如下程序段:
int n=1,sum=0;
while(sum<=32767) {sum+=n; n++;}
printf(“n=%d\n”,n-1);
乍看该程序时无错误,但事实上,上列程序中的while循环是一个无限循环,原因在于int型数的表示X围为-32768到+32767,当累加和sum超过32767时,便向高位进位,而对int型数而言,最高位表示符号,故sum超过32767后便得到一个负数,while条件当然满足,从而形成无限循环。此时,最好的解决办法是将sum定义为long int型。
(4)词法分析中的“贪心法”
C语言中的某些符号,例如/、*、=、+等,只有一个字符长,称为单词符符
号。而/*、==、++等包含了多个字符,称为多字符符号。当C编译器读入一个字符‘/’后又跟了一个字符‘*’,那么编译器就必须做出判断:是将其作为两个分别的符号对待,还是合起来作为一个符号对待。C语言的处理策略是“贪心法”,即从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如可能,再读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号为止。所以a---b:应理解为(a--)-b;而将描述命题x除以p所指向的值时,应书写为:y=x/(*p);而不要写为:y=x/*p,因为编译器将/*理解为一段注释的开始。
六、C语言常见错误小结
C语言的最大特点是:功能强、使用方便灵活。C编译的程序对语法检查并不象其它高级语言那么严格,这就给编程人员留下“灵活的余地”,但还是由于这个灵活给程序的调试带来了许多不便,尤其对初学C语言的人来说,经常会出一些连自己都不知道错在哪里的错误。看着有错的程序,不知该如何改起,本人通过对C的学习,积累了一些C编程时常犯的错误,写给各位学员以供参考。
1.书写标识符时,忽略了大小写字母的区别。
main()
{
int a=5;
printf("%d",A);
}
编译程序把a和A认为是两个不同的变量名,而显示出错信息。C认为大写字母和小写字母是两个不同的字符。习惯上,符号常量名用大写,变量名用小写表示,以增加可读性。
2.忽略了变量的类型,进行了不合法的运算。
main()
{
float a,b;
printf("%d",a%b);
}
%是求余运算,得到a/b的整余数。整型变量a和b可以进行求余运算,而实型变量则不允许进行“求余”运算。
3.将字符常量与字符串常量混淆。
char c;
c="a";
在这里就混淆了字符常量与字符串常量,字符常量是由一对单引号括起来的单个字符,字符串常量是一对双引号括起来的字符序列。C规定以“”作字符串结束标志,它是由系统自动加上的,所以字符串“a”实际上包含两个字符:‘a‘和‘‘,而把它赋给一个字符变量是不行的。
4.忽略了“=”与“==”的区别。
编译器错误
在许多高级语言中,用“=”符号作为关系运算符“等于”。如在BASIC程序中可以写
i f (a=3) then …
但C语言中,“=”是赋值运算符,“==”是关系运算符。如:
if (a==3) a=b;
前者是进行比较,a是否和3相等,后者表示如果a和3相等,把b值赋给a。由于习惯问题,初学者往往会犯这样的错误。