第一篇已经讨论了很多典型错误,可以供学习参考。本节主要列举一些典型问题。这里没有用错误一词,而改用问题,就是说设计的语句本身没有错误,但没有达到要求,或者虽然达到要求,但存在效率问题。
16.3.1 没有涵盖全部条件
有时没有仔细审题,漏掉了控制程序执行的条件。请看下面的例子。
【例16.4】这个程序对输出的数据进行适当运算之后,如果a<b,则交换它们的值,然后输出两个数的关系。请找出该程序的问题。
#include <stdio.h>int main( ){ int a=0,b=0; printf("输入两个整数:",a,b); scanf("%d%d",&a,&b); a=(a=(5*a,3*a ),a+9); if(a<b) { a+=b;b=a-b;a-=b; } printf("%d>%d/n",a,b); return 0;}
这个程序只是判断“a<b”的情况,忽视了“a=b”的情况,运行时会产生如下结果:
输入两个整数:0 09>9
可能会有人问为何输出语句没有区分“a>b”和“a<b”的情况,其实在“a<b”的情况里,已经调整为“a>b”,所以只需一个输出语句。
在对a和b进行运算之后,先增加如下判定条件:
if(a==b) printf("%d=%d/n",a,b);
运行结果为:
输入两个整数:0 99=99>9
由此可见,第2个的输出语句变成两者的公用语句。可能有人认为只要简单地将这条输出语句限制在“a<b”的复合语句之中执行即可,即:
if(a<b) { a+=b;b=a-b;a-=b; printf("%d>%d/n",a,b);}
这是行不通的,因为破坏了原来的正确路径,当“a>b”,不需要执行第2个判断语句时,就没有相应的输出语句。即当输入“2-2”时,就不执行输出语句,造成错误。
为了不影响原来的输出,应该在输出“a=b”之后,直接结束程序的运行。下面就是修改后的正确程序。
#include <stdio.h>int main( ){ int a=0,b=0; printf("输入两个整数:",a,b); scanf("%d%d",&a,&b); a=(a=(5*a,3*a ),a+9); if(b>a) b-=a; else b+=a; if(a==b) { printf("%d=%d/n",a,b); return 0; } if(a<b) { a+=b;b=a-b;a-=b; } printf("%d>%d/n",a,b); return 0;}
三种情况的运行示范如下。
输入两个整数:2 -215>13输入两个整数:2 015=15输入两个整数:5 933>24
需要注意的是,不能将程序的第2个分支部分修改为如下形式:
if(a<b) printf("%d>%d/n",b,a);else printf("%d>%d/n",a,b);
这样的修改虽然保证了输出结果正确,但不符合原程序的要求,即交换a和b的值。
【例16.5】下面是一个求复数除法的程序。
typedef struct { double re, im;}complex;complex p(complex x, complex y){ double d; complex z; d = y.re*y.re + y.im*y.im; if(d==0) return z; z.re = (x.re * y.re + x.im*y.im)/d; z.im = (x.im * y.re - x.re*y.im)/d; return( z );}#include <stdio.h>void main ( ){ complex a,b,c; a.im=0; a.re=1; b.im=1.0; b.re=1.0; c=p(a,b); printf("%lf + %lfi/n",c.re,c.im);}
这个程序的输出为:0.500000+-0.500000i
要求程序的输出为:0.500000-0.500000i
修改程序的设计,使它满足需要。这个程序能处理除数为零的情况吗?
【解答】要解决这个问题,可以简单地使用判断语句判断虚部的符号位,例如:
if(c.im >= 0) printf("%lf + %lfi/n", c.re, c.im);else printf("lf - %lfi/n", c.re, -c.im);
也可以将符号存入一个字符型的变量中,作为符号位。假设符号位为flag,当符号为正时,flag的值为'+',符号为负时,其值为'/0'。这时就可以使用统一的输出语句
printf("%lf%c%lfi/n", c.re, flag, c.im);
不过,它的输出格式没有前者灵活。
在主程序中,没有区分除数为零的情况,所以这个程序不能处理除数为零的情况。
在除法程序中,当除数为零,执行语句
if(d == 0) return z;
时,z是没有被初始化的,主程序的输出语句将输出随机数字。可能有的人认为可以使用语句
return 1;
来解决这个问题。其实,这样也是不行的。因为p返回结构类型的函数,返回1变成返回整数值,与原来的类型不符,编译就会报错。对错误处理是使用exit函数,即
exit(1);
如果使用exit函数,需要增加头文件并修改函数名,这里暂不使用这种方法。
在p函数里将z初始化为0,当除数为0时,给出除数为零的信息并设置z.re为-1,通过语句
return z;
直接退出函数,即改为
if(d==0) { printf("除数为零,结束运行。/n"); z.re=-1; return z;}
因为只是退出p函数,所以主程序里还需要处理除数为零的信息。这可以用p函数里面为z设置的信息来处理,即
if(c.re == -1) return 0;
完整的程序如下。
#include <stdio.h>typedef struct { double re, im;}complex;complex p(complex x, complex y){ double d; complex z; z.re=0; z.im=0; d = y.re*y.re + y.im*y.im; if(d==0) { printf("除数为零,结束运行。/n"); z.re=-1; return z; } z.re = (x.re * y.re + x.im*y.im)/d; z.im = (x.im * y.re - x.re*y.im)/d; return( z );}int main ( ){ complex a,b,c; a.im=0; a.re=1; b.im=1.0; b.re=1.0; c=p(a,b); if(c.re==-1) return 0; if(c.im>=0) printf("%lf + %lfi/n",c.re,c.im); else printf("%lf - %lfi/n",c.re, -c.im); return 0;}
如果使用exit函数,就可以直接在函数里结束程序运行,免去在主函数里还要判别的重复动作。不过,这时不能再使用p函数名,p的名字在stdlib.h中已经有定义,所以把函数名改为pe。使用字符变量flag存储符号。
完整的程序如下。
#include <stdlib.h>#include <stdio.h>typedef struct { double re, im;}complex;complex pe(complex x, complex y){ double d; complex z; d = y.re*y.re + y.im*y.im; if(d==0) { printf("被除数为零,结束运行。/n"); exit(1); } z.re = (x.re * y.re + x.im*y.im)/d; z.im = (x.im * y.re - x.re*y.im)/d; return( z );}int main ( ){ complex a,b,c; char flag='/0'; printf("输入第1个复数:"); scanf("%lf%lf",&a.re,&a.im); printf("输入第2个复数:"); scanf("%lf%lf",&b.re,&b.im); c=pe(a,b); if(c.im>=0) { flag='+'; printf("%lf%c%lfi/n",c.re,flag,c.im); } else printf("%lf%c%lfi/n",c.re,flag,c.im); return 0;}
程序示范运行后的输出结果如下。
输入第1个复数:1 0输入第2个复数:1 10.500000-0.500000i输入第1个复数:4 3输入第2个复数:2 12.200000+0.400000i输入第1个复数:5 9输入第2个复数:0 0除数为零,结束运行。
其实,如果是自己编写程序,一般都要自力更生,不要依靠被调用的程序。也就是在自己组织输入数据时,应该剔除不合理的数据。
16.3.2 条件超出范围
这种情况是指设计的条件过宽,超出范围而造成程序运行结果不正确。
【例16.6】这是计算具有从1开始的10个自然数的数组前5项之和的程序,要求将计算结果用如下形式输出。
1+2+3+4+5=15
下面是它的源程序。
#include <stdio.h>int main ( ){ int a[ ]={1,2,3,4,5,6,7,8,9,10}; int i=0, sum=0; for(i=0;i<5;i++){ sum=sum+a[i]; printf("%d+",a[i]); } printf("=%d/n",sum);}
这个程序的实际输出为:
1+2+3+4+5+=15
由输出结果可见,比要求多输出一个“+”号。一种方法是在for循环体内解决,例如将1条输出语句改为if~else语句实现。
if(i != 4) printf("%d+",a[i]);else printf("%d",a[i]);
另一种方法是按计算顺序解决,把最后一次的计算剥离出去单独处理,这就不需要判断语句了。完整的程序如下。
#include <stdio.h>int main ( ){ int a[ ]={1,2,3,4,5,6,7,8,9,10}; int i=0, sum=0; for(i=0;i<4;i++){ sum=sum+a[i]; printf("%d+",a[i]); } printf("%d=%d/n",a[i],sum+a[i]); return 0;}
【例16.7】下面的程序用来求1+2+3+…+n≤10000时的最大的n值。
#include <stdio.h>int main( ){ int sum, i; sum=0; i=0; while(sum<=10000) { ++i; sum+=i; } printf("n=%d/n",i); return 0;}
程序输出为:
n=141
这个计算结果并不正确,因为语句
sum<=10000
判定的条件是必要条件。计算肯定要使条件满足才能停止循环。符合条件时的i值已经超过1次,所以应该减去1次,即140次。
可能有人以为使用do~while不用减1,其实是一样的,因为循环的条件一样。后者虽然先计算后判别条件,但不满足大于10000时,它会继续循环。一旦满足,当然就已经多计算一次了。下面是do~while的演示程序。
#include <stdio.h>int main( ){ int sum=0; int i=0; do{ ++i; sum+=i; }while(sum<=10000); // 循环结束时,i会多加1而sum会多加i printf("1+2+3+…+%d=%d/n",i-1, sum-i); // 减去多记的部分 return 0;}
运行结果如下:
1+2+3+…+140=9870
其实,while和do~while还是有细微区别的,稍不注意也会使输出结果不符合要求。请看下面两个例子的比较。
【例16.8】计算两个数字之差,直到输入数字为0时停止计算。
#include <stdio.h>int main( ){ int a,b,x; printf("输入两个数字:"); do { scanf ( "%d %d",&a,&b ); x=a-b; printf ( "x=%d/n", x ); printf("输入两个数字:"); } while ( a!=0&&b!=0 ); printf("退出程序!/n"); return 0;}
运行结果如下:
输入两个数字:6 89x=-83输入两个数字:0 8x=-8输入两个数字:退出程序!
显然,这个程序输出了不需要的信息。问题在于它先执行运算后判断条件。可以推知,这个程序开始执行时,如运行实例所示,当输入数字中有一个为0,则输出结果就含多余信息。
应该先判断再执行,修改的程序如下。
#include <stdio.h>int main( ){ int a,b,x; printf("输入两个数字:"); scanf ( "%d %d", &a,&b ); while ( a!=0 && b!=0 ) //任意一个为0则退出 { x=a-b; printf ( "x=%d/n", x ); printf("输入两个数字:"); scanf ( "%d %d", &a,&b ); } printf("退出程序!/n"); return 0;}
运行示范如下:
输入两个数字:0 9退出程序!输入两个数字:5 68x=-63输入两个数字:89 3x=86输入两个数字:8 0退出程序!
16.3.3 减少循环次数
这种情况是指设计的程序运行结果正确,但应该改进以提高效率。一般是针对循环控制而言,即应减少循环的次数以提高程序的效率。这里举几个典型的例子进行比较说明。
1.寻找逃犯
【例16.9】一辆汽车撞人后逃跑,4个目击者提供如下线索:
甲:牌照三、四位相同;
乙:牌号为31xxxx;
丙:牌照五、六位相同;
丁:三~六位是一个整数的平方。
为了从这些线索中求出牌照号码,只要求出后四位再加上310000即可。这个四位数又是前两位相同,后两位也相同,互相又不相同并且是某个整数的平方。利用计算机计算速度快的特点,把所有可能的方式都试一下,从中找出符合条件的数。这就是所谓的穷举法。参考程序如下:
#include <stdio.h>void main( ){ int i,j,k,c; for(i=1; i<=9; i++) for(j=0; j<=9; j++) if(i!=j) { k=i*1000+i*100+j*10+j; for(c=1; c*c<k; c++); if(c*c==k) printf("牌照号码是: %ld。/n",310000+k); }}
运行输出如下:
牌照号码是:317744
因为后面4位数,1000的平方根大于31,所以穷举实验时,变量c不需从1开始,而可以从31开始寻找一个整数的平方。为了提高效率,for语句可以改为如下形式:
for(c=31; c*c<k; c++);
2.百钱买百鸡问题
【例16.10】设每只母鸡值3元,每只公鸡值2元,两只小鸡值1元。现要用100元钱买100只鸡,问能同时买到母鸡、公鸡、小鸡各多少只?
如果要求程序在找到解的同时,输出循环的次数。希望寻找一个循环次数较少的算法。
设母鸡、公鸡、小鸡分别为i、j、k只,则可以列出如下两个方程:
这里有3个未知数,所以是一个不定方程。要求同时买到母鸡、公鸡、小鸡,也就是给出一个限制条件:任何一个不能为0。这需要使用三重循环,通过枚举找出所有符合条件的解答。
小鸡需要从2开始,每次增加2。由于已经考虑让i和j从1开始枚举,所以不需要判别如下附加条件:
i*j*k!=0//参考程序#include <stdio.h>int main( ){ int m=0,n=0,sum=0; int i,j,k; for(i=1;i<100;i++) { ++sum; for(j=1; j<100;j++) { ++sum; for(k=2;k<100;k=k+2) { ++sum; m=i+j+k; n=3*i+2*j+k/2; if((m==100)&&(n==100)) { printf("母鸡:%2d 公鸡:%2d 小鸡:%2d/n",i,j,k); } } } } printf("一共循环%d次。/n",sum); return 0;}
程序运行结果如下:
母鸡: 2 公鸡:30 小鸡:68母鸡: 5 公鸡:25 小鸡:70母鸡: 8 公鸡:20 小鸡:72母鸡:11 公鸡:15 小鸡:74母鸡:14 公鸡:10 小鸡:76母鸡:17 公鸡: 5 小鸡:78一共循环490149次。
其实,第3层就循环了480249次。
考虑到母鸡为3元一只,100元都买母鸡,最多也只能买33只。要求每个品种都要,小鸡只能为偶数,因此最多为30只,即第一循环变量i可从1到30。
公鸡为2元一只,最多能买50只。因为至少需要1只母鸡和2只小鸡,所以公鸡不会超出50-3=47(只)。因循环时已经决定枚举的母鸡数i,一只母鸡相当1.5只公鸡,所以第二层循环时,公鸡j只要从1到47-1.5i即可。
因为i+j+k=100,所以直接求得k=100-i-j,不再需要第3层循环,即:
k=100-i-j; if(3*i+2*j+0.5*k==100) printf("母鸡:%2d 公鸡:%2d 小鸡:%2d/n", i, j, k);//改进的算法#include <stdio.h>int main( ){ int k=0,sum=0; int i,j; for(i=1;i<=30;i++) { ++sum; for( j=1; j<=47-1.5*i;j++) { ++sum; k=100-i-j; if(3*i+2*j+0.5*k==100) { printf("母鸡:%2d 公鸡:%2d 小鸡:%2d/n",i,j,k); } } } printf("一共循环%d次。/n",sum); return 0;}
程序运行结果如下:
母鸡: 2 公鸡:30 小鸡:68母鸡: 5 公鸡:25 小鸡:70母鸡: 8 公鸡:20 小鸡:72母鸡:11 公鸡:15 小鸡:74母鸡:14 公鸡:10 小鸡:76母鸡:17 公鸡: 5 小鸡:78一共循环735次。
其中第二层循环705次。
3.鸡兔同笼
【例16.11】大约在1500年前,《孙子算经》中记载了一个有趣的问题。书中是这样叙述的:“今有鸡兔同笼,上有三十五头,下有九十四足,问鸡兔各几何?”
解答思路是这样的:假如砍去每只鸡、每只兔一半的脚,则每只鸡就变成了“独角鸡”,每只兔就变成了“双脚兔”。由此可知:
(1)鸡和兔的脚的总数就由94只变成了47只;
(2)如果笼子里有一只兔子,则脚的总数就比头的总数多1。因此,脚的总只数47与总头数35的差,就是兔子的只数,即47-35=12(只)。
(3)知道兔子的只数,则鸡的只数为:35-12=23(只)。
这一思路新颖而奇特,其“砍足法”也令古今中外数学家赞叹不已。这种思维方法叫化归法。化归法就是在解决问题时,先不对问题采取直接的分析,而是将题中的条件或问题进行变形,使之转化,直到最终把它归成某个已经解决的问题。
下面使用计算机来求解鸡兔同笼问题。
设鸡为i只,兔为j只,则有:
使用i和j分别表示两层循环,逐次枚举试验,当满足上述条件时,就可求出鸡有i只,兔有j只。下面是按此思想编写的程序,sum表示执行循环的总次数。
//鸡兔同笼#include <stdio.h>int main(){ int sum=0; int i,j; for( i=1;i<35; i++) { sum++; for( j=1;j<35;j++) { sum++; if((i+j==35)&&(2*i+4*j==94)) printf("鸡有%d只,兔有%d只。/n",i,j); } } printf("一共循环%d次。/n",sum); return 0;}
程序运行结果如下:
鸡有23只,兔有12只。一共循环1190次。
其实,第二个循环执行1156次。由此可见,这个循环次数很大,所以应该减少第二个循环的次数。如果将它改为“j=35-i”,则会降为595次。
通过分析鸡兔关系,可以改进程序的效率。
(1)两只鸡和一只兔子的脚数相等,所以鸡头的数量不会超过三分之二,即i<25,j<13。
(2)给定一个i,j的初始值应该是35-i。
//改进的算法#include <stdio.h>int main(){ int sum=0; int i,j; for( i=1;i<24; i++) { sum++; for( j=35-i;j<13;j++) { sum++; if((i+j==35)&&(2*i+4*j==94)) printf("鸡有%d只,兔有%d只。/n",i,j); } } printf("一共循环%d次。/n",sum);}
程序运行结果如下:
鸡有23只,兔有12只。一共循环24次。
其实,要等到j=35-i<13时,才进入第二个循环,而且仅执行1次。
由这两个例子可见,循环次数的控制很重要。
5.复制字符串
【例16.12】下面是一个复制字符串的程序,找出提高效率的解决方法。
#include <stdlib.h>#include <stdio.h>char *mycopy(char *dest,char *src){ if(dest == NULL || src == NULL) return dest; while(*dest++=*src++); return dest;}void main ( ){ char s2[16]="how are you?", s1[16]=" "; mycopy(s1,s2); printf(s1); //输出how are you? printf("/n");}
【分析】程序主要的开销是while语句。在语句
while(*dest++=*src++);
中,首先要对src指针变量进行读操作,读出src所指向的地址,再对这个地址进行读操作。同样,对dest指针变量也要进行类似操作,读出dest所指向的地址,再对这个地址进行写操作。即对变量本身有两次读操作,根据对变量所指向的地址,进行读写操作,还要分别执行“++”操作,总共进行6次操作。
由于它分别对dest和src进行3次操作,造成效率降低。假设地址相差len,即有
int len=dest-src;while(*(src+len)=*src++);
这就把对目标地址dest的访问,变成对源地址src访问的一个增量len,则以后只要读一次内存,再加上这个源地址的增量len,就可以代替对目的地址的访问。这就将6次操作降为4次,提高了效率。
//提高效率的程序#include <stdlib.h>#include <stdio.h>char *mycopy(char *dest,char *src){ int len=dest-src; if(dest == NULL || src == NULL) return dest; while(*(src+len)=*src++); return dest;}void main ( ){ char s2[16]="how are you?" , s1[16]=" "; mycopy(s1,s2); printf(s1); printf("/n");}
但是这个办法仍然是1个1个字节地复制字符串。内存是按32位,4个字节存储数据的,使用整数指针,就可以按4字节访问字符串。
【例16.13】演示按4个字节赋值字符串的例子。
#include <stdio.h>void main ( ){ char s2[32]="0123456789ABCDEF12"; char s1[32]=" "; int *p, *p2, i=0; p=(int*)s2; p2=(int*)s1; *p2=*p; for(i=0;i<5;i++,*p2=*(p+i)) printf("%s,%x,%x/n",(char*)p2,*p2,*(p+i));}
这个程序只是演示使用整数指针p2每次从字符串中得到4个字符的过程。注意要将字符类型指针强制转换为整数指针,第1次使用“*p2=*p;”使得*p2获得p指向地址里第1个4字节字符。在for语句中使用“*(p+i)”读取下一个4字节内容。第5次只有2个字节(字符1和2)内容,但它还有一个结束符‘/0’,所以实际是3个字节的有效内容。
从下面给出程序输出结果可知,输出分为3列,左边是赋值给*p2的4个字符,中间是这4个字符所对应的ASCII编码。右边是*(p+i)的内容,也用ASCII码表示,所以与中间的内容完全一样。
0123,33323130,333231304567,37363534,3736353489AB,42413938,42413938CDEF,46454443,4645444312,3231,3231
从这里得到启示,先把字符按4个字节复制,不足4字节则按位复制,这样就会大大提高效率。这里先使用对源字符串求长度的方法,为了完成复制字符串,需要同步移动指针p和p2。
【例16.14】演示按4个字节复制字符串的例子。
#include <stdio.h>void main ( ){ char s2[32]="0123456789ABCDEF12"; char s1[32]=" "; char *cp=s2; int *p,*p2,i=0,len=0; while(*cp!='/0') ; { len++; cp++; } len++; printf("%d/n",len); if(len%4==0) len=len/4; else len=len/4+1; printf("%d/n",len); p=(int*)s2; p2=(int*)s1; for(i=0;i<len;i++){ *p2=*p; printf("%s,%x,%x/n",(char*)p2,*p2,*(p+i)); p2++;p++; } printf(s1); printf("/n");}
程序增加求字符串长度和循环次数的调试信息。复制输出仍然分为3列,最后输出复制给字符数组s1的内容。
1950123,33323130,333231304567,37363534,4241393889AB,42413938,3231CDEF,46454443,012,3231,12ffc00123456789ABCDEF12
包含头文件“string.h”就可使用库函数strlen求字符串长度,即
len=strlen(s2)+1;
如果设计一个宏,专门用来判断是否是一个完整的4字节,如果是,则按4字节复制,否则按逐字节复制,这样就可以简化程序的设计,下面给出完整的例子。
【例16.15】使用宏来实现按4个字节复制字符串的参考程序。
#include <stdlib.h>#include <stdio.h>#define CONTAIN_OF_ZERO_BYTE(n) /(((n-0x01010101) & (~n)) & 0x80808080)char *mycopy(char *dest,char *src){ int len=dest-src; int *d,*s; d=(int *)dest; //强制转换成整数指针类型 s=(int *)src; //强制转换成整数指针类型 if(dest == NULL || src == NULL) return dest; while(1) { if(!CONTAIN_OF_ZERO_BYTE(*s)) { printf("*s%x ",*s); //准备复制4个字节的内容 printf("%s/n",(char *)(s+1)); //尚没有完成复制的字符串 *d=*s; s++; d++; continue; } src=(char *)s; //强制转换回字符指针类型 printf("*src%x %s/n",*src,src+1); //演示逐字节复制 while(*(src+len)=*src++) printf("*src%x %s/n",*src,src+1); //演示逐字节复制 break; } return dest;}void main ( ){ char s2[32]="0123456789ABCDEF12"; char s1[32]=" "; mycopy(s1,s2); printf(s1); printf("/n");}
程序中加入的输出信息是为了帮助理解执行过程。选择特殊的字符串也是为了能容易看出复制过程。
程序运行输出如下:
*s33323130 0123456789ABCDEF12*s37363534 456789ABCDEF12*s42413938 89ABCDEF12*s46454443 CDEF12*src31 12*src32 2*src00123456789ABCDEF12
程序输出的左边是每次复制给目标数组的内容,右边是尚没有复制的内容。执行4次以后,剩下"12'/0'",改为一个一个地复制,共执行3次。
这类例子很多,着眼点都是减少循环的次数,不再赘述。