讀古今文學網 > C語言解惑 > 19.1 函數變量的作用域 >

19.1 函數變量的作用域

變量作用域根據起作用的範圍分為對一個函數、一個程序、一個文件及整個程序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中,則不需要再聲明。