变量作用域根据起作用的范围分为对一个函数、一个程序、一个文件及整个程序4个层次。要特别注意分辨各个层次的处理方法。
19.1.1 块结构之间的变量屏蔽规则
C语言规定,任何以花括号“{”和“}”括起来的复合语句都属于块结构,在块内可以对变量进行定义。块结构用在同一个函数内,遵循变量屏蔽原则。
1.块结构定义错误
【例19.1】找出下面程序中的错误,改正后分析它的输出结果。
#include <stdio.h>int max(int,int);int c=108;int main ( ){ { int a=45,b=98,c=0; c=max(45,98); { int c=0; for(int i=0; i<11;i++) c=c+i; c *= c; c=max(c,98); printf (/"max=%dn/",c); } printf (/"max=%dn/",c); c=max(a,-c); printf (/"max=%dn/",c); } printf (/"max=%dn/",c); return 0; } int max(int a, int b) { static int c; if (a<b) c=b; else c=a; return c; }
首先排除max函数,这个函数的声明和定义均正确。它虽然使用变量c,但与主函数里定义的变量c以及全局变量c均没有关系,所以要在块内寻找错误。顺便说一句,这个函数设计得不好,它的目的只是想混淆视听,以便对c的定义产生错觉。
程序第1次调用max函数求得c=98。然后进入下一个块内。
C语言规定可以在多个块内定义同名的变量,而且遵循变量定义原则,即在执行语句之前定义。for语句中的循环体“()”内不算块结构,而且它是执行语句,所以不能在该语句的循环体内定义变量(C++语言可以,但C语言不行)。将这3行语句改写如下:
int c=0,i=1;for(; i<11;i++) //等效for(i=1; i<11;i++) c=c+i;
这里定义的变量c屏蔽了上层定义的c,计算得出c=55。自乘之后为3025,调用max函数,最大值就是3025,第1个输出语句的输出为:
max=3025
程序返回上一层块内,丢弃n内层的c值,输出原来的c值,即
max=98
这时选c的负值作为参数,肯定最大值是正45,输出为
max=45
再返回一层,只有全局变量起作用,所以输出为
max=108
结论:在块内定义的变量其作用域仅限于块内。若块内定义与块外或外部定义具有相同的变量名,则它们是没有关系的。变量必须在程序开始时声明或者定义。推而广之,本块使用的变量必须在本块开始时定义。
2.正确理解块结构定义的变量之作用范围
【例19.2】一个源程序的清单如下:
#include <stdio.h>int max(int,int);int c;int main ( ){ { int a=45,b=98; static int sum; sum=max(45,98); a+=b; { int a={1,-2,3,-4,5},i=0; static int sum; for(i=0; i<5;i++) if(a[i]<0) c=c+a[i]; else sum=sum+a[i]; printf (/"正数和=%d,负数和=%dn/",sum,c); } c=max(a,sum); printf (/"max=%dn/",c); c=a+b+sum; } printf (/"总和=%dn/",c); return 0;}int max(int a, int b){ if (a<b) return b; else return a;}
有两人对这个程序进行分析,分别给出如下结果。
甲:这个程序是错误的,原因是变量c和sum没有初始化,计算的结果不定。
乙:这个程序是对的。变量c和sum均被初始化为0。第1个输出语句为:
正数和=9,负数和=-6
因为a+b之和大于sum,所以第2个输出语句为:
max=143
因为c=a+b+sum=143+98+9=250,所以最后一个输出为:
总和=250
请分析他们哪位说的正确?
都不正确。乙对第1个输出语句判断正确。全局变量c和静态变量sum都被初始化为0值。他对第2个输出的判断结果是对的,那只是数据的偶然性。当离开该块时,在该块定义的静态变量sum的值与普通变量的一样,都自动消失。这时起作用的是上一块的同名变量,即这时的sum=98。98<143,所以输出结果与乙给出的一样。但在求c值时仍然用到sum,所以他的计算结果就错了,应该是c=143+98+98=339。正确的输出为:
总和=339
变量屏蔽原则:编译程序为块内的自动型变量动态分配存储空间。具体地说,是将这些自动型变量使用的堆栈空间在进入块内时就给予分配,一旦退出该块,分配给它的空间就立即消失(即这些自动型变量消失),所以自动型变量既不能被块外的变量或函数所引用,也不能保存其值。当各块具有同名的自动型变量时,屏蔽其他块定义的同名变量,只有本块定义的自动变量起作用;当退出该块后仍为当前所在块的同名变量起作用。当自动型变量与某外部型变量具有相同的名字时,只有块中定义的自动型变量起作用;当退出该块后仍为外部变量起作用。
结论:复合块中,外层不能使用内层定义的变量,内层可以使用外层的非同名变量,各层使用自己的同名变量。非同名外部变量可供各层的程序使用。
19.1.2 程序和文件内的变量
如果程序很小,一个程序可能只有一个主函数。不过一般来说,一个源文件含有多个函数。为此,将它们都作为程序文件看待。
1.正确初始化变量
【例19.3】检查出下面程序中的错误并改正之。
#include <stdio.h>int fac (int);int main ( ){ int i; for ( i=1; i<=4; i++) printf ( /"%d !=%dn/",i, fac(i) ); return 0;}int fac ( int n){ static int f= 1; int i=1; for ( i=1; i<=n; i++) f=f*i; return(f);}
函数错误地定义“static int f=1;”,静态变量只初始化一次,这就使它的初值总保持为上一个阶乘值,而不是1。当计算3!的时候,就得到3!=12的错误结果。计算结果错误为:
1!=12!=23!=124!=288
这时调用阶乘函数,运行结果应该为:
1! = 12! = 23! = 64! = 24
应将这条语句改为定义一个自动型变量“int f=1;”。
一定要注意变量的初始化规则。例如对如下的程序块:
{ int x; static int y; static int z=5;}
块中的简单类型的局部变量x没有初始化,x有不确定的初始值,y被说明为静态的,所以为0,z初始化为5。
变量初始化规则:只能对外部和静态变量做一次初始化工作,从概念上看应在编译时进行。自动型和寄存器型变量,每进入函数或复合语句一次,就被初始化一次,而且初值不限于常数,可以包含以前已定义过的值,甚至包含函数调用的合法表达式。
如没有明显地进行初始化,则C编译程序对变量的初始化规则是:
(1)外部型和静态型变量初始值为0;
(2)自动型和寄存器型变量初始值为随机数。
2.同文件内的同名变量作用域
【例19.4】分析下面程序的输出结果。
#include <stdio.h>extern int a;int b=50;void func1(void);void func2(void);void main( ){ int i; for ( i=1; i<4; i++) { ++a; printf ( /"%dt/",a ); printf ( /"%dt/",b++ ); func1(); func2(); }}int a=10;void func1(){ ++a; printf ( /"%dt/",a );}void func2(){ int a=100; int b=15; ++a; printf (/"%dt%dn/",a,++b );}
【分析】首先要分清变量的类型。变量a是外部变量,主函数main及函数func1都使用它,所以两个函数的运行都影响变量a的数值。变量b也是全局变量,主函数main使用printf函数调用它,函数func1不使用它。但函数func2里定义了与全局变量同名的变量a和b,所以它使用自己的变量,对全局变量没有影响。
分析的关键是主程序循环调用func1和func2函数3次。既然函数func2里也定义了一个本函数使用的同名变量a和b,所以它们只执行加1操作,输出总是101和16。
变量b是全局变量,但只有主函数main使用printf函数调用它,而且是“b++”运算,所以第1次输出是b的初始值50,随后两次循环输出51和52。
复杂一点的是变量a,它在主函数后面定义是一样的,不影响main函数使用它。主函数里对它执行加1操作,输出11。func2使用这个a值,做加1操作,输出12。后面就是重复执行两次,主函数输出13和15,func1函数输出14和16。
由此分析,可得到如下运行结果:
11 50 12 101 1613 51 14 101 1615 52 16 101 16
由此可见,外部变量在整个程序中都可存取,它提供了在函数间进行数据通信的另一种方法。只要将用作函数间通信的参数说明为外部变量,而在函数定义的形式参数表中和调用函数的实参表中不需要给出,在函数中只要直接对这些外部变量进行操作即可。使用的屏蔽原则与块变量的相同。
结论:外部型变量可以被程序中的所有函数引用。外部型变量实质上具有“全局型”定义,它的作用域是整个程序。如果有同名变量,则只有内部变量起作用。
如果要在定义一个外部型变量之前使用它,就必须使用关键字extern进行声明。程序中的变量a,在没赋值时主程序就引用它,所以使用关键字extern进行声明。
3.同文件内不允许有同名函数
【例19.5】分析下面程序的输出结果。
#include <stdio.h>#include <stdlib.h>int p(int,int);int main( ){ int a=50,b=2; printf(/"%dn/",p(a,b)); return 0;}int p(int a, int b){ return a/b;}
可能有人会马上回答“输出25”。其实,因为stdlib.h中有个与p同名的函数,所以这个程序通不过编译。解决的方法有两种:因为这个程序用不到这个头文件,所以可以删除。另一种是为函数p改名。
19.1.3 多文件变量作用域
【例19.6】这个源程序包括两个文件。在VC中的工程项目如图19-1所示。
各个文件的内容如下:
//c19_6.c#include <stdio.h>int main( ){ int a=50,b=2; extern char *str; printf(/"%s%dn/",str,pe(a,b)); return 0;}//c19_61.cchar str=/"a/b=/";int pe(int a, int b){ return a/b;}
图19-1 双文件示意图
程序编译出错,请分析程序存在的错误。
【解答】主程序所在文件没有声明引用c19_61.c文件中的函数pe。在多文件编程中,也不允许有同名函数,如果引用别的文件中的函数时,则需要声明被引用函数的原型。
文件中的各个函数不能使用其他文件函数中的变量(所以各个函数内的变量可以同名),如果使用另一个文件的全局变量,则必须声明。c19_6.c中虽然声明外部变量,但声明的格式不对。c19_61.c将str定义为字符数组,c19_6.c也应该使用
extern char str;
方式,不能声明为字符指针。main函数在运行到这个位置时,应该读该地址的前4位。前4位是字符,不是地址,所以出错。
可能有人说,字符数组和字符指针不是可以互换吗?确实可以,但有特例,这就是少有的特例之一。
修改后的文件如下:
//c19_6.c#include <stdio.h>int pe(int,int);int main( ){ int a=50,b=2; extern char str; printf(/"%s%dn/",str,pe(a,b)); return 0;}//c19_61.cchar str=/"a/b=/";int pe(int a, int b){return a/b;}
不过并不推荐这种文件组织方式,而是推荐将公共变量定义在头文件中,使用它的源文件用包含语句将其包含即可。注意使用双引号,不要使用尖括号。
注意:在多文件编程中,一定使用双引号包含自定义的头文件。
现在将字符串直接定义在main函数中,三个文件的内容组织如下:
//c19_6.h#include <stdio.h>int pe(int,int);extern char str;//c19_6.c#include /"c18_6.h/"int main( ){ int a=50,b=2; char str=/"a/b=/"; printf(/"%s%dn/",str,pe(a,b)); return 0;}//c19_61.cint pe(int a, int b){return a/b;}
图19-2是其示意图。
图19-2 包含头文件的示意图
运行结果如下:
a/b=25
【例19.7】这个源程序包括5个文件,有4个c文件和1个头文件。编译对这个文件第1次扫描时,没有警告信息,但第2次扫描时出错。请找出错误并改正之。
这个程序使用独立的c文件编写求两个实数的最大值、最小值和平均值的函数,这些函数除了返回值之外,还要将自己被调用的次数加到总计数器上。
头文件声明它们的函数原型,主函数接收2个输入值,输出最大值、最小值、平均值、平均值的2倍和调用函数的总次数。具体要求源程序如下:
//主文件find.c//作用:调用各个函数。#include <stdio.h>#include /"find.h/"int count=0; //初始化计数变量countvoid main( ){ double a,b; printf(/"Input a and b:n/"); scanf(/"%lf%lf/",&a,&b); printf ( /"max =%lfn/",max( a,b )); printf ( /"min=%lfn/",min( a,b )); printf ( /"mean=%lfn/",mean( a,b )); printf ( /"2*mean=%lfn/", K*mean( a,b )); //使用常数K printf ( /"count=%dn/",count);}
为了观察count,两次调用mean函数。定义常数K为double型,便于计算。
//头文件find.h的作用:声明函数原型及外部变量double max(double,double);double min(double,double);double mean(double, double);extern const double K=2; //声明全局常变量Kextern int count; //声明全局计数变量count//文件max.c的内容:求最大值函数max#include /"find.h/"double max(double m1, double m2){ count=count+1; if(m1 > m2) return m1; else return m2;}//文件min.c的内容:求最小值函数min#include /"find.h/"double min(double m1, double m2){ count=count+1; if(m1 > m2) return m2; else return m1;}//文件mean.c的内容:求平均值函数mean#include /"find.h/"double mean(double m1, double m2){ count=count+1; return (m2+m1)/K; //使用常数K}
【解答】常数K不能那样定义。如果使用
#define K 2
定义一个常量K是可以的。但这个K没有数据类型的概念,所以不推荐使用这种方法。这里使用const时,应该参考普通变量的使用方法。不应该在头文件中定义,应该声明为外部常量,即
extern const double K; //声明全局常变量K
然后在find.c中定义如下:
const double K=2; //定义常数K为double型
图19-3的右边是主文件find.c的示意图,左边是文件结构图。
图19-3 修改后的find.c示意图
//修改后的头文件find.hdouble max(double,double);double min(double,double);double mean(double, double);extern const double K; //声明全局常变量Kextern int count; //声明全局计数变量count
运行示范如下:
Input a and b:50 90max =90.000000min=50.000000mean=70.0000002*mean=140.000000count=4
推荐:将函数原型全部声明在头文件中,公共变量也声明在头文件中,然后在需要的地方加以定义。当然,只能在一个地方定义,一定不能重复定义。
【例19.8】工程文件c19_8包含c19_8.c和c19_81.c两个文件,请找出错误并改正之。
//c19_8.c#include <stdio.h>int number=25;extern void display(void);int main( ){ display(); return 0;}//c19_81.c#include <stdio.h>int number=25;void display( ){ printf(/"%dn/",number); return;}
【解答】两个文件的函数里可以有同名变量,并且单独使用互不影响。但同名的全局变量只能在一个文件里声明,在另一个文件里定义,不能同时定义。将c19_81.c里的变量改为外部变量的声明即可,即
extern int number;
其实最好的设计方法是使用头文件。下面是按此要求设计的源程序。
//c19_8.h#include <stdio.h>void display(void);extern int number;//c19_8.c#include /"c19_8.h/"int number;int main( ){ printf(/"输入:/"); scanf(/"%d/",&number); display(); return 0;}//c19_81.c#include /"c19_8.h/"void display( ){ printf(/"%dn/",number); return;}
两个文件的公共变量number,在头文件里声明,在另一个文件里定义。这里有意改为在主函数里通过键盘赋值,所以在文件开始处还需要声明一次,否则通不过第2次扫描。而在文件c19_81.c中,则不需要再声明。