诊断错误及其处理.docx
《诊断错误及其处理.docx》由会员分享,可在线阅读,更多相关《诊断错误及其处理.docx(22页珍藏版)》请在冰豆网上搜索。
诊断错误及其处理
一、诊断错误及其处理
防止程序出错的第一关是编译器。
如果遇到约束违规的情况或语法错误,编译器至少会生成一个诊断错误信息。
大多数编译器将其诊断信息分为两类:
错误和警告。
语法错误很常见,也较易修改。
调试程序时首先要改正的是语法错误,调试本身是一种艺术,是程序中断时试着去修复的一种艺术。
糟糕的语法会使编译器混乱,甚至可能达到生成很多错误的程度,程序设计者对这种情况不要大惊小怪,应冷静对待,其实很可能仅仅是由于某一个语句引起的,比如说漏掉了while循环中的一个大括号或语句末尾键入了冒号而非分号等。
为此我们应该养成一种习惯,自顶向下的修改方法,每次从错误表的开头开始,一次修改一、二个错误再编译,或许错误就能减少许多甚至全部语法错误。
模块化程序设计也有助于程序的排错。
调试一个充满连接和转折、全局变量等的程序要比调试一个精心设计的模块化程序困难得多。
如果程序分成几个模块,各个模块负责程序的一个专项功能,一旦识别出问题可能所处的模块,就很容易地通过检查源代码方式来发现错误,这种策略也称为分治法,因为如果知道了没有问题之处,几乎等同于知道了问题发生的地方。
有时我们也可以忽略警告,但这并非是一种良好的编程习惯,追踪每个警告的原因并认真考虑是否有更稳健的方法编写代码能够帮助编程者编写更好的代码。
如果我们只是忽略它,当这些“无害”的警告不断累积到一定程度时,可能面临出现混乱的危险。
二、差1错误及其处理
在C语言中,一个拥有n个元素的数组,不存在下标为n的元素,其元素下标的允许取值范围为0到n-1。
请考察下列一段代码:
inta[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-oneerror)。
“差一错误”也称“栏杆错误”,问题说的是:
100英尺长的围栏每隔10英尺需要一根支撑用的栏杆,一共需要多少根呢?
如果不假思索,将100除以10得到10,当然这个答案是错误的,正确的答案应该是11。
是否存在一些编程技巧,能够降低这类错误发生的可能性呢?
“不对称边界”法给程序设计带来的便利非常明显。
在“不对称边界”法中,可以规定数组元素的第一个“入界点”(对C语言而言,0就是数组下标的第一个“入界点”)和第一个“出界点”(对上述定义的数组而言,第一个出界点就是10,它不在数组下标范围之内)。
在这种方法下,“出界点”的值即是数组的长度。
三、“悬挂”else错误及其处理
这个问题并非C语言所独有,也已经为人熟知,但即使是有经验的C程序员,也常常在此失误。
如果想实现当x分别是大于0、等于0和小于0时y值分别取得1、0和-1。
考虑下面的程序片段:
y=0;
if(x>=0)
if(x>0)y=1;
elsey=-1;
然而,这段代码实际上所做的与编程者的愿望相去甚远。
原因在于C语言中有这样的规则,else总是与同一对括号内最近的未匹配的if结合。
如果我们按照上面这段程序实际上被执行的逻辑来调整代码缩进,大致是这样:
y=0;
if(x>=0)
if(x>0)y=1;
elsey=-1;
也就是说,并非是当x<0时y=-1,而是当x=0时也使y=-1。
解决这一问题我们可以用“封装”的办法,将上述程序改为:
y=0;
if(x>=0)
{if(x>0)y=1;}
elsey=-1;
现在,else与第一个if结合,即使它离第二个if更近也是如此,因为此时第二个if已经被括号“封闭”起来了。
四、整数溢出及其处理
C语言为编程者提供了三种不同长度的整数:
shortint、int和longint,但不管是哪种类型表示的整数总有一定的范围,越出该范围时称为整数的溢出。
例如现有算法要求如下:
求满足条件1+2+3+…+n≤32767的最大整数n,请考察如下程序段:
intn=1,sum=0;
while(sum<=32767){sum+=n;n++;}
printf(“n=%d\n”,n-1);
乍看该程序时无错误,但事实上,上列程序中的while循环是一个无限循环,原因在于int型数的表示范围为-32768到+32767,当累加和sum超过32767时,便向高位进位,而对int型数而言,最高位表示符号,故sum超过32767后便得到一个负数,while条件当然满足,从而形成无限循环。
此时,最好的解决办法是将sum定义为longint型。
五、词法陷井及其处理
(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语言为编程者提供了三种不同长度的整数:
shortint、int和longint,但不管是哪种类型表示的整数总有一定的范围,越出该范围时称为整数的溢出。
例如现有算法要求如下:
求满足条件1+2+3+…+n≤32767的最大整数n,请考察如下程序段:
intn=1,sum=0;
while(sum<=32767){sum+=n;n++;}
printf(“n=%d\n”,n-1);
乍看该程序时无错误,但事实上,上列程序中的while循环是一个无限循环,原因在于int型数的表示范围为-32768到+32767,当累加和sum超过32767时,便向高位进位,而对int型数而言,最高位表示符号,故sum超过32767后便得到一个负数,while条件当然满足,从而形成无限循环。
此时,最好的解决办法是将sum定义为longint型。
(4)词法分析中的“贪心法”
C语言中的某些符号,例如/、*、=、+等,只有一个字符长,称为单词符符号。
而/*、==、++等包含了多个字符,称为多字符符号。
当C编译器读入一个字符‘/’后又跟了一个字符‘*’,那么编译器就必须做出判断:
是将其作为两个分别的符号对待,还是合起来作为一个符号对待。
C语言的处理策略是“贪心法”,即从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如可能,再读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号为止。
所以a---b:
应理解为(a--)-b;而将描述命题x除以p所指向的值时,应书写为:
y=x/(*p);而不要写为:
y=x/*p,因为编译器将/*理解为一段注释的开始。
六、C语言常见错误小结
C语言的最大特点是:
功能强、使用方便灵活。
C编译的程序对语法检查并不象其它高级语言那么严格,这就给编程人员留下“灵活的余地”,但还是由于这个灵活给程序的调试带来了许多不便,尤其对初学C语言的人来说,经常会出一些连自己都不知道错在哪里的错误。
看着有错的程序,不知该如何改起,本人通过对C的学习,积累了一些C编程时常犯的错误,写给各位学员以供参考。
1.书写标识符时,忽略了大小写字母的区别。
main()
{
inta=5;
printf("%d",A);
}
编译程序把a和A认为是两个不同的变量名,而显示出错信息。
C认为大写字母和小写字母是两个不同的字符。
习惯上,符号常量名用大写,变量名用小写表示,以增加可读性。
2.忽略了变量的类型,进行了不合法的运算。
main()
{
floata,b;
printf("%d",a%b);
}
%是求余运算,得到a/b的整余数。
整型变量a和b可以进行求余运算,而实型变量则不允许进行“求余”运算。
3.将字符常量与字符串常量混淆。
charc;
c="a";
在这里就混淆了字符常量与字符串常量,字符常量是由一对单引号括起来的单个字符,字符串常量是一对双引号括起来的字符序列。
C规定以“”作字符串结束标志,它是由系统自动加上的,所以字符串“a”实际上包含两个字符:
‘a‘和‘‘,而把它赋给一个字符变量是不行的。
4.忽略了“=”与“==”的区别。
在许多高级语言中,用“=”符号作为关系运算符“等于”。
如在BASIC程序中可以写
if(a=3)then…
但C语言中,“=”是赋值运算符,“==”是关系运算符。
如:
if(a==3)a=b;
前者是进行比较,a是否和3相等,后者表示如果a和3相等,把b值赋给a。
由于习惯问题,初学者往往会犯这样的错误。
5.忘记加分号。
分号是C语句中不可缺少的一部分,语句末尾必须有分号。
a=1
b=2
编译时,编译程序在“a=1”后面没发现分号,就把下一行“b=2”也作为上一行语句的一部分,这就会出现语法错误。
改错时,有时在被指出有错的一行中未发现错误,就需要看一下上一行是否漏掉了分号。
{z=x+y;
t=z/100;
printf("%f",t);
}
对于复合语句来说,最后一个语句中最后的分号不能忽略不写(这是和PASCAL不同的)。
6.多加分号。
对于一个复合语句,如:
{z=x+y;
t=z/100;
printf("%f",t);
};
复合语句的花括号后不应再加分号,否则将会画蛇添足。
又如:
if(a%3==0);
I++;
本是如果3整除a,则I加1。
但由于if(a%3==0)后多加了分号,则if语句到此结束,程序将执行I++语句,不论3是否整除a,I都将自动加1。
再如:
for(I=0;I<5;I++);
{scanf("%d",&x);
printf("%d",x);}
本意是先后输入5个数,每输入一个数后再将它输出。
由于for()后多加了一个分号,使循环体变为空语句,此时只能输入一个数并输出它。
7.输入变量时忘记加地址运算符“&”。
inta,b;
scanf("%d%d",a,b);
这是不合法的。
Scanf函数的作用是:
按照a、b在内存的地址将a、b的值存进去。
“&a”指a在内存中的地址。
8.输入数据的方式与要求不符。
①scanf("%d%d",&a,&b);
输入时,不能用逗号作两个数据间的分隔符,如下面输入不合法:
3,4
输入数据时,在两个数据之间以一个或多个空格间隔,也可用回车键,跳格键tab。
②scanf("%d,%d",&a,&b);
C规定:
如果在“格式控制”字符串中除了格式说明以外还有其它字符,则在输入数据时应输入与这些字符相同的字符。
下面输入是合法的:
3,4
此时不用逗号而用空格或其它字符是不对的。
343:
4
又如:
scanf("a=%d,b=%d",&a,&b);
输入应如以下形式:
a=3,b=4
9.输入字符的格式与要求不一致。
在用“%c”格式输入字符时,“空格字符”和“转义字符”都作为有效字符输入。
scanf("%c%c%c",&c1,&c2,&c3);
如输入abc
字符“a”送给c1,字符“”送给c2,字符“b”送给c3,因为%c只要求读入一个字符,后面不需要用空格作为两个字符的间隔。
10.输入输出的数据类型与所用格式说明符不一致。
例如,a已定义为整型,b定义为实型
a=3;b=4.5;
printf("%f%dn",a,b);
编译时不给出出错信息,但运行结果将与原意不符。
这种错误尤其需要注意。
11.输入数据时,企图规定精度。
scanf("%7.2f",&a);
这样做是不合法的,输入数据时不能规定精度。
12.switch语句中漏写break语句。
例如:
根据考试成绩的等级打印出百分制数段。
switch(grade)
{case‘A‘:
printf("85~100n");(不知道为什么这里的反斜线打不出来!
)
case‘B‘:
printf("70~84n");(不知道为什么这里的反斜线打不出来!
)
case‘C‘:
printf("60~69n");(不知道为什么这里的反斜线打不出来!
)
case‘D‘:
printf("<60n");(不知道为什么这里的反斜线打不出来!
)
default:
printf("errorn");(不知道为什么这里的反斜线打不出来!
)
由于漏写了break语句,case只起标号的作用,而不起判断作用。
因此,当grade值为A时,printf函数在执行完第一个语句后接着执行第二、三、四、五个printf函数语句。
正确写法应在每个分支后再加上“break;”。
例如
case‘A‘:
printf("85~100n");break;(不知道为什么这里的反斜线打不出来!
)
13.忽视了while和do-while语句在细节上的区别。
(1)main()
{inta=0,I;
scanf("%d",&I);
while(I<=10)
{a=a+I;
I++;
}
printf("%d",a);
}
(2)main()
{inta=0,I;
scanf("%d",&I);
do
{a=a+I;
I++;
}while(I<=10);
printf("%d",a);
}
可以看到,当输入I的值小于或等于10时,二者得到的结果相同。
而当I>10时,二者结果就不同了。
因为while循环是先判断后执行,而do-while循环是先执行后判断。
对于大于10的数while循环一次也不执行循环体,而do-while语句则要执行一次循环体。
14.定义数组时误用变量。
intn;
scanf("%d",&n);
inta[n];
数组名后用方括号括起来的是常量表达式,可以包括常量和符号常量。
即C不允许对数组的大小作动态定义。
15.在定义数组时,将定义的“元素个数”误认为是可使的最大下标值。
main()
{staticinta[10]={1,2,3,4,5,6,7,8,9,10};
printf("%d",a[10]);
}
C语言规定:
定义时用a[10],表示a数组有10个元素。
其下标值由0开始,所以数组元素a[10]是不存在的。
16.初始化数组时,未使用静态存储。
inta[3]={0,1,2};
这样初始化数组是不对的。
C语言规定只有静态存储(static)数组和外部存储(exterm)数组才能初始化。
应改为:
staticinta[3]={0,1,2};
17.在不应加地址运算符&的位置加了地址运算符。
scanf("%s",&str);
C语言编译系统对数组名的处理是:
数组名代表该数组的起始地址,且scanf函数中的输入项是字符数组名,不必要再加地址符&。
应改为:
scanf("%s",str);
18.同时定义了形参和函数中的局部变量。
intmax(x,y)
intx,y,z;
{z=x>y?
x:
y;
return(z);
}
形参应该在函数体外定义,而局部变量应该在函数体内定义。
应改为:
intmax(x,y)
intx,y;
{intz;
z=x>y?
x:
y;
return(z);
附:
TurboC(V2.0)编译错误信息
说明:
TurboC的来源程序错误分为三种类型:
致命错误、一般错误和警告。
其中,致命错误通常是内部编译发生错误;一般错误指程序的语法错误、磁盘或内存存取错误或指令行错误等;警告则只是指出一些得怀疑的情况,它并不防止编译的进行。
下面按字母顺序A~Z分别列出致命错误及一般错误讯息,英汉对照及处理方法:
(一)、致命错误英汉对照及处理方法:
A-B致命错误
Badcallofin-linefunction(内部函数非法调用)
分析与处理:
在使用一个宏定义的内部函数时,没能正确调用。
一个内部函数以两个加底线(__)开始和结束。
Irreducableexpressiontree(不可约表达式树)
分析与处理:
这种错误指的是档案行中的表达式太复杂,使得编码产生程序无法为它产生编码。
这种表达式必须避免使用。
Registerallocationfailure(内存分配失败)
分析与处理:
这种错误指的是档案行中的表达式太复杂,编码产生程序无法为它产生编码。
此时应简化这种繁杂的表达式或干脆避免使用它。
(二)、一般错误讯息英汉照及处理方法
#operatornotfollowedbymacoargumentname(#运算符后没跟宏变元名)
分析与处理:
在宏定义中,#用于标示一宏变串。
"#"号后必须跟一个宏变元名。
'xxxxxx'notanargument('xxxxxx'不是函数参数)
分析与处理:
在源程序中将该标示符定义为一个函数参数,但此标示符没有在函数中出现。
Ambiguoussymbol'xxxxxx'(二义性符号'xxxxxx')
分析与处理:
两个或多个结构的某一域名相同,但具有的偏移、类型不同。
在变量或表达式中引用该域而未带结构名时,会产生二义性,此时需修改某个域名或在引用时加上结构名。
Argument#missingname(参数#名遗失)
分析与处理:
参数名已脱离用于定义函数的函数原型。
如果函数以原型定义,该函数必须包含所有的参数名。
Argumentlistsyntaxerror(参数表出现语法错误)
分析与处理:
函数调用的参数间必须以逗号隔开,并以一个右括号结束。
若原始档中含有一个其后不是逗号也不是右括号的参数,则发生错误。
Arrayboundsmissing(数组的界限符"]"遗失)
分析与处理:
在原始档中定义了一个数组,但此数组没有以下右方括号结束。
Arraysizetoolarge(数组太大)
分析与处理:
定义的数组太大,超过了可用内存空间。
Assemblerstatementtoolong(汇编语句太长)
分析与处理:
内部汇编语句最长不能超过480位。
Badconfigurationfile(设定档不正确)
分析与处理:
TURBOC.CFG设定文件中包含的不是合适指令行选择项的非批注文字。
设定文件指令选择项必须以一个短横线开始。
Badfilenameformatinincludedirective(包含指令中文件名称格式不正确)
分析与处理:
包含文件名称必须用引号("filename.h")或尖括号()括起来,否则将产生本类错误。
如果使用了宏,则产生的延伸文字也不正确,因为无引号没办法识别。
Badifdefdirectivesyntax(ifdef指令语法错误)
分析与处理:
#ifdef必须以单个标示符(只此一个)作为该指令的体。
Badifndefdirectivesyntax(ifndef指令语法错误)
分析与处理:
#ifndef必须以单个标示符(只此一个)作为该指令的体。
Badundefdirectivesyntax(undef指令语法错误)
分析与处理:
#undef指令必须以单个标示符(只此一个)作为该指令的体。
Badfilesizesyntax(位字段长语法错误)
分析与处理:
一个位字段长必须是1-16位的常量表达式。
Callofnon-functin(调用未定义函数)
分析与处理:
正被调用的函数无定义,通常是由于不正确的函数声明或函数名拼错而造成。
Cannotmodifyaconstobject(不能修改一个长量对像)
分析与处理:
对定义为常量的对象进行不合法操作(如常量配置)引起本错误。
Caseoutsideofswitch(Case出现在switch外)
分析与处理:
编译程序发现Case语句出现在switch语句之外,这类故障通常是由于括号不符合造成的。
Casestatementmissing(Case语句漏掉)
分析与处理:
Case语必须包含一个以冒号结束的常量表达式,如果漏了冒号或在冒号前多了其它符号,则会出现此类错误。
Characterconstanttoolong(字符常量太长)
分析与处理:
字符常量的长度通常只能是一个或两个字符长,超过此长度则会出现这种错误。
Compoundstatementmissing(漏掉复合语句)
分析与处理:
编译程序扫瞄到原始文件未时,未发现结束符号(大括号),此类故障通常是由于大括号不符合所致。
Conflictingtypemodifiers(类型修饰符冲突)
分析与处理:
对同一指针,只能指定一种变址修饰符(如near或far);而对于同一函数,也只能给出一种语言修饰符(如Cdecl、pascal或interrupt)。
Constantexpressionrequired(需要常量表达式)
分析与处理:
数组的大小必须