變量作用域根據起作用的範圍分為對一個函數、一個程序、一個文件及整個程序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 !=1 2 !=2 3 !=12 4 !=288
這時調用階乘函數,運行結果應該為:
1 ! = 1 2 ! = 2 3 ! = 6 4 ! = 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 16 13 51 14 101 16 15 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.c char 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.c char 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.c int 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 ; // 初始化計數變量count void 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 ; // 聲明全局常變量K extern 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.h double max (double ,double ); double min (double ,double ); double mean (double , double ); extern const double K ; // 聲明全局常變量K extern int count ; // 聲明全局計數變量count
運行示範如下:
Input a and b : 50 90 max =90.000000 min=50.000000 mean=70.000000 2*mean=140.000000 count=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中,則不需要再聲明。