拙劣的外部变量
/*
样本代码.c
————谭浩强 ,《C程序设计》(第四版),清华大学出版社, 2010年6月,p201~202
*/
#include <stdio.h>
float Max=0,Min=0;
int main()
{ float average(float array[],int n);
float ave,score[10];
int i;
printf("Please enter 10 scores:");
for(i=0;i<10;i++)
scanf("%f",&score[i]);
ave=average(score,10);
printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n",Max,Min,ave);
return 0;
}
float average(float array[],int n)
{ int i;
float aver,sum=array[0];
Max=Min=array[0];
for(i=1;i<n;i++)
{if(array[i]>Max)Max=array[i];
else if(array[i]<Min)Min=array[i];
sum=sum+array[i];
}
aver=sum/n;
return(aver);
}
这段代码的功能很清楚:输入10个float类型数据,求这10个数的平均值,最大值,最小值并输出。
但是这段代码中的拙劣之处很多初学者却很难察觉,甚至中等程度的编程者恐怕也说不清楚。
为了揭示这段代码的拙劣,首先来看一下初学者的代码一般是什么样的。
很多初学者往往习惯于在main()函数中解决所有问题,完成所有的任务。所以一个水平不高的初学者解决这个问题很可能给出这样的代码
初学者的代码.c
/* 初学者的代码.c */ #include <stdio.h> int main ( void ) { float array[10]; float sum,max,min; int i; printf("Please enter 10 scores:"); for( i = 0 ; i < 10 ; i++ ) scanf("%f",&array[i]); sum = max = min = array[0]; for( i = 1 ; i < 10 ; i++ ) { if(array[i]>max)max = array[i]; else if(array[i]<min)min = array[i]; sum += array[i]; } printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , max , min , sum / 10 ) ; return 0; }
对比一下不难发现,“初学者的代码.c”比“样本代码.c”要强得多。这反映在如下几个方面,变量的数量:前者只用了5个,后者却用了10个;代码总长度:前者不到20行,后者接近30行;速度,前者显然比后者更快,因为不需要average()函数调用的种种开销。总之,样本代码在几乎各个方面都比不上初学者的代码。所以结论很明显,样本代码显然是一段质量低下的代码。
如果对“初学者的代码.c”进行改进,相信不会有人想到把max,min这两个变量改成外部变量,因为这毫无意义。由此不难看出“样本代码.c”中定义“float Max=0,Min=0;”不但荒谬而且无聊。
唯一能为“样本代码.c”找回一点面子的大概是它的main()函数比“初学者的代码.c”的main()函数要稍微短一点,这一点确实成立。那么为了average()函数能一次计算出三个值而设立Max,Min这两个外部变量是否能说得过去呢?答案是不能。理由如下:
1.如果是为了average()函数能一次计算出三个值而设立Max,Min这两个外部变量,其实不如索性设立三个外部变量:
更合理的外部变量.c
/*更合理的外部变量.c*/ #include <stdio.h> void calculate(float [],int ); float Max , Min , Ave ; int main( void ) { float score[10]; int i; printf("Please enter 10 scores:"); for(i=0;i<10;i++) scanf("%f",&score[i]); calculate(score,10); printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n",Max,Min,Ave); return 0; } void calculate(float array[],int n) { int i; float sum = array[0]; Max = Min = array[0]; for(i=1;i<n;i++) { if(array[i]>Max)Max=array[i]; else if(array[i]<Min)Min=array[i]; sum += array[i]; } Ave = sum / n ; }
它比样本代码少用了1个变量,同时又免除了return的开销。显然更优于“样本代码.c”。因此,“样本代码.c”中定义2个外部变量显然是不伦不类的。
2.如果定义外部变量和average()函数的目的是为了让main()函数更短些的话(简洁的main()函数更容易在整体上把握程序的正确性),其实main()函数可以更短些,下面的代码根本就不应该写在main()函数中:
for(i=0;i<10;i++)
scanf("%f",&score[i]);
因为它与average(score,10);根本就不是同一层次上的事情。这样,代码应该是
比更合理更合理的代码.c
/*比更合理更合理的代码.c*/ #include <stdio.h> void calculate(float [],int ); void input(float [],int ); float Max , Min , Ave ; int main( void ) { float score[10]; int i; printf("Please enter 10 scores:"); input(score,10); calculate(score,10); printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n",Max,Min,Ave); return 0; } void input(float array[],int n) { int i; for(i=0;i<n;i++) scanf("%f",&array[i]); } void calculate(float array[],int n) { int i; float sum = array[0]; Max = Min = array[0]; for(i=1;i<n;i++) { if(array[i]>Max)Max=array[i]; else if(array[i]<Min)Min=array[i]; sum += array[i]; } Ave = sum/n ; }
这个代码的main()函数显然比“样本代码.c”的更简洁更清晰。
因此不难得出结论,即“样本代码.c”并非是对“初学者的代码.c”的改进,与后者相比,它是一种退化而绝非进化。如果试图对代码进行进一步改进,只能以“初学者的代码.c”作为起点,而不能以“样本代码.c”作为起点。
那么“初学者的代码.c”应该如何改进呢?有两个方向
改进一,提高速度。注意到main()函数中有两个基本一样的循环,如果合并成一个则会缩短程序运行时间。
改进一.c
/*改进一.c*/ #include <stdio.h> int main ( void ) { float array[10]; float sum,max,min; int i; printf("Please enter 10 scores:"); scanf("%f",&array[0]); sum = max = min = array[0]; for( i = 1 ; i < 10 ; i++ ) { scanf("%f",&array[i]); if(array[i]>max)max = array[i]; else if(array[i]<min)min = array[i]; sum += array[i]; } printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , max , min , sum / 10 ) ; return 0; }
免除了函数调用的开销,这个程序显然应该更快一点。然而问题是,这样做值得吗?为了丁点的速度,把输入、求最大值、求最小值、求数组元素的和,眉毛胡子一把抓地搅和在一起,实际上破坏的是代码的整洁、清晰和可维护性,失大于得。国内的ACMer们特别喜欢干这种事情,他们往往会为了亿万分之一秒的速度不惜败坏程序的整体结构。就犹如从牙缝里抠出了点食物残渣,然后炫耀他们比别人吃的更多一些。因此他们的代码往往就跟那啥一样,都是一次性使用的,不具备任何可复用价值。
是不是说速度不重要呢?倒也不是。速度作为程序的一个性能指标当然必须满足。但是追求速度不能以破坏代码的可读性和可维护性为代价,这就如同说钱很重要但你不能去抢银行一样。如果一辆汽车,速度比别的车每公里快0.1秒,那能算快吗?而且这0.1秒的代价是汽车非常容易抛锚,每次抛锚都得修个一年半载的,能说这个汽车好吗?要知道任何一辆汽车的维护期都比制造期长得多的多,软件也是如此。
程序的速度一般应该从最初的总体设计着手,软件、硬件配置以及软件的总体设计方案,并且应该留有余地,就如同桥梁的设计承载要有一个很大的安全系数一样。在代码级别也并非不应该忽视速度,但不能以破坏整体结构为代价。你总不能为了减轻桥梁的自重而把桥墩里的钢筋少加几根吧?
既然把几件事搅在一起是一种糟糕的代码风格,那么良好的风格就应该是把几件事分开:
改进二.c
/*改进二.c*/ #include <stdio.h> int main ( void ) { float array[10]; float sum , max ,min ; int i; printf("Please enter 10 scores:"); for( i = 0 ; i < 10 ; i++ ) scanf("%f",&array[i]); for( i = 0 , max = array[0] ; i < 10 ; i++ ) if( array [i] > max ) max = array[i]; for( i = 0 , min = array[0] ; i < 10 ; i++ ) if( array[i] < min ) min = array[i]; for( i = 0 , sum = 0 ; i < 10 ; i++ ) sum += array[i]; printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , max , min , sum / 10 ) ; return 0; }
这个改进虽然失去了一些速度,但相应的补偿是main()函数的结构非常清晰——一个简单的顺序结构。这样的代码可读性好,也易于维护。但是这个改进依然有不足,就是main()函数太长,代码过于细致。解决的办法是使用函数。
改进三.c
/*改进三.c*/ #include <stdio.h> void input(float [],int ); float find_max(float [],int ); float find_min(float [],int ); float find_ave(float [],int ); int main ( void ) { float array[10]; printf("Please enter 10 scores:"); input ( array , 10 ); printf( "max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , find_max( array , 10 ) , find_min( array , 10 ) , find_ave( array , 10 ) ) ; return 0; } float find_ave( float array[],int n ) { int i; float sum ; for( i = 0 , sum = 0 ; i < n ; i++ ) sum += array[i]; return sum/n ; } float find_min( float array[],int n ) { int i; float min ; for( i = 0 , min = array[0] ; i < n ; i++ ) if( array[i] < min ) min = array[i]; return min ; } float find_max(float array[],int n) { int i; float max ; for( i = 0 , max = array[0] ; i < n ; i++ ) if( array [i] > max ) max = array[i]; return max ; } void input(float array[],int n) { int i; for(i=0;i<n;i++) scanf("%f",&array[i]); }
与“改进二.c”相比,这个程序的速度更慢。因为函数这种东西从来不是为了提高运行速度而存在的,它提高的是开发的效率和维护的效率。所以片面追求运行速度往往是一种顾此失彼的行为,而且失大于得。
使用函数使main()函数变得无以伦比的清晰,换句话说,具有高度的可读性和可维护性。而每个函数,由于很小,不但具有很好的可读性和可维护性,正确性也更容易得到保证。这就是结构化程序设计的目标,也是其必然结果。
不难发现,在“改进三.c”中,input()、find_max()、find_min()、find_ave()这几个函数的实参完全相同,也就是说它们(也包括main()函数)都围绕着一个共同的数据——array数组工作,这种情况下可以对代码做进一步的形式上的简化,即使用外部变量。使用了外部变量可以省略形参和实参。
改进四.c
/*改进四.c*/ #include <stdio.h> void input(void); float find_max(void); float find_min(void); float find_ave(void); float array[10]; int main ( void ) { printf("Please enter 10 scores:"); input (); printf( "max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , find_max() , find_min() , find_ave() ) ; return 0; } float find_ave( void ) { int i; float sum ; for( i = 0 , sum = 0 ; i < 10 ; i++ ) sum += array[i]; return sum/10 ; } float find_min( void ) { int i; float min ; for( i = 0 , min = array[0] ; i < 10 ; i++ ) if( array[i] < min ) min = array[i]; return min ; } float find_max(void) { int i; float max ; for( i = 0 , max = array[0] ; i < 10 ; i++ ) if( array [i] > max ) max = array[i]; return max ; } void input(void) { int i; for( i = 0 ; i < 10 ; i++ ) scanf("%f",&array[i]); }
这样写的代价是破坏了程序的整体结构,各个函数互相依赖,失去了可复用性。整个程序不再是“改进三.c”那样的高内聚、低耦合的范式,而是整个地耦合到了一起。至于得失的估计,恐怕是见仁见智。但好歹是有所得:代码中不写实参、形参,省去了传递参数的开销。考察“样本代码.c”,不难发现那两个外部变量用得不伦不类,没有得到外部变量的任何好处,只收获了使用外部变量的害处。
几个函数共享数据,在代码中可以免写实参形参,是使用外部变量的唯一能摆到桌面上的理由。但是这种方式只适应于一些小型的、特定类型的(整个程序就是围绕一套公共的数据进行加工)问题。在多数情况下,使用外部变量的坏处远甚于滥用goto语句,因为goto影响的是一个函数局部,但外部变量影响的则是整个程序。使用外部变量就如同把计算机主板上的插卡牢牢地焊接在主板上一样,看起来似乎你不用再担心插卡松动了,但你收获的烦恼则更多。
很多初学者因为不熟悉函数的写法而图省事使用外部变量是万万要不得的做法,那样绝对妨碍技术的提高,永远写不出优秀的代码。可以这么说,不熟悉函数就根本不可能懂得编程。
“改进四.c”这个代码中,main()函数至少在形式上与array数组无关,所以进一步的改进应该是把它与这段代码中的其他部分剥离开,最后用两个模块,一个写array数组及与其直接相关的函数,另一个写main()函数。
或许,有的初学者因为无法实现“更合理的外部变量.c”中通过调用calculate()一次实现计算三个值的功能而使用外部变量,实际通过一次函数调用实现三个值的计算的正规写法应该是利用指针。
/* 利用指针.c */ #include <stdio.h> void input(float [],int ); void calculate(float [],int ,float *,float *,float *); int main ( void ) { float array[10]; float ave,max,min; int i; printf("Please enter 10 scores:"); input(array , 10 ); calculate(array , 10 , &max , &min , &ave ); printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , max , min , ave ) ; return 0; } void calculate(float array[],int n ,float *p_max , float * p_min ,float *p_ave) { int i; *p_ave = *p_max = * p_min = array[0]; for(i = 1 ; i < n ; i++) { if(array[i] > *p_max ) *p_max = array[i]; if(array[i] < *p_min ) *p_min = array[i]; *p_ave += array[i]; } *p_ave /= n ; } void input(float array[],int n) { int i; for( i = 0 ; i < n ; i++ ) scanf("%f",&array[i]); }
不难看出,实现这一功能根本不需要外部变量。如果嫌这样传递的参数太多的话,可以利用结构体。
/*利用结构体.c */ #include <stdio.h> typedef struct {float ave,max,min;} FEATURES ; void input(float [],int ); void calculate(float [],int , FEATURES * ); int main ( void ) { float array[10]; FEATURES features; int i; printf("Please enter 10 scores:"); input(array , 10 ); calculate(array , 10 , &features ); printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n" , features.max , features.min , features.ave ) ; return 0; } void calculate(float array[],int n ,FEATURES *p_f ) { int i; p_f->ave = p_f->max = p_f->min = array[0]; for(i = 1 ; i < n ; i++) { if(array[i] > p_f->max ) p_f->max = array[i]; if(array[i] < p_f->min ) p_f->min = array[i]; p_f->ave += array[i]; } p_f->ave /= n ; } void input(float array[],int n) { int i; for( i = 0 ; i < n ; i++ ) scanf("%f",&array[i]); }
当然,利用下面类型的函数也可以实现类似的功能:
FEATURES calculate( float array[],int n ) { /*求值,return 返回值;*/ }