首页 » C语言解惑 » C语言解惑全文在线阅读

《C语言解惑》16.3 设计存在的问题

关灯直达底部

第一篇已经讨论了很多典型错误,可以供学习参考。本节主要列举一些典型问题。这里没有用错误一词,而改用问题,就是说设计的语句本身没有错误,但没有达到要求,或者虽然达到要求,但存在效率问题。

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次。

这类例子很多,着眼点都是减少循环的次数,不再赘述。