C語言中識別符號的作用域、名稱空間、連結屬性、生命週期、儲存型別

林堯彬發表於2020-04-04

        無論學習哪一種語言,都免不了要討論這些問題。而且這些問題,深究起來有時也讓我們很迷惑。

       識別符號的定義無需多講,只需注意不僅僅是指變數,還有函式標籤等。

1. 識別符號的作用域

        作用域是指允許對識別符號進行訪問的位置範圍。按照C99(章節6.2.1),C語言的作用域共有 4 種型別:檔案作用域、程式碼塊作用域、函式作用域、函式原型作用域。

型別

位置

說明

檔案作用域  (file)在所有 程式碼塊和引數列表 之外整個檔案內都可以訪問
程式碼塊作用域 ( block)在“程式碼塊”或者“函式的引數列表”內部只有所在的程式碼塊內可以訪問
函式作用域 (function)函式體內具有此作用域的只有一種語句:只有goto語句要使用的“語句標籤”。簡化為一條規則:一個函式中的語句標籤(即label)不可相同。
函式原型作用域  (function prototype)宣告的函式原型的引數列表中(注意與“函式定義”不同)由於函式原型的引數名稱可以省略,即使不省略,也不要求和“函式定義”中的形參列表中名稱相同。 
只有一種情況會發生衝突:引數列表中的有重複的變數名。(這時編譯報錯: redefinition of parameter )

        說明:當出現兩個識別符號名稱相同的情況,而且都屬於同一個名稱空間,那麼在內層程式碼塊,內層的那個識別符號會隱藏外層的那個識別符號。

舉例說明並分析

  1. int my_func(int a, int b);  /* myfunc是“檔案作用域”;a,b是 “函式原型作用域” */   
  2. int a;/* a是檔案作用域。 注意:雖然上面的函式原型中將引數名稱宣告為a, 但是由於作用域不同,是合法的。下一行的b也是這種情況 */   
  3. static int b; /* b是檔案作用域 */   
  4. int d( int n ){ /* d是“檔案作用域”。因為這是函式定義,而不是函式原型,所以形式引數n 是“程式碼塊作用域” */  
  5.                 /* 由於形式引數中已經宣告n,那麼在函式體內的最外層變數的名稱就不能再為n,因為同一個作用域內不允許對同一個變數進行多次宣告。  
  6.                       如果宣告,編譯器會提示重複宣告變數。(在某些較老版本的編譯器是允許的,但是C99標準是不允許的)  
  7.                     在不同的作用域內可以 */  
  8.     int f;  /* f是程式碼塊作用域 */  
  9.       
  10.     int g(int k);   /* 函式原型,位於函式體程式碼塊內。宣告的函式名稱g是“程式碼塊作用域”,引數k是“函式原型作用域” */  
  11.     
  12.   my_label:  /* 定義一個label,是“函式作用域” */       
  13.      
  14.     ...  /*  下面的程式碼塊可以是while迴圈、for迴圈或if語言等等*/   
  15.     {    
  16.         int f, g, i; /* 都是程式碼塊作用域,而且只是在內層程式碼塊,在外層程式碼塊不可見 */  
  17.                     /* 對於f,外層已經存在f,這裡會隱藏掉外層的f,即在這個內層程式碼塊中無法訪問外層的f */  
  18.         int n;     /* 程式碼塊作用域,由於這裡已經不是函式體內的最外層,所以可以宣告與函式的形式引數同名的變數, 
  19.                         同樣會隱藏掉外層的變數n   */   
  20.     }  
  21.     ...  /* 另外一個 程式碼塊 */   
  22.     {  
  23.         int i;  /* 程式碼塊作用域,雖然上面的一個內層程式碼塊中已經存在i,但是由於這兩個程式碼塊不存在巢狀關係,所以也不存在隱藏現象 */  
  24.     }  
  25. }  

    注意事項:

         1.  注意函式原型中的引數是“函式原型作用域”,而函式定義中的引數是“程式碼塊作用域”。例如上面程式碼中第一行的a,b和函式定義中的 n

         2.  由於函式定義中引數是“程式碼塊作用域”,所以在函式體內的最外層的變數名稱不能再為n,但是內層巢狀的程式碼塊變數名稱可以為n。雖然這條特性在某些較老版本的編譯器中是可以的,但是在ANSI C中師不允許的。

         3.  變數的隱藏只是針對巢狀的作用域,對於不巢狀的作用域就沒有這個說法。例如上面例子中的變數 f 是巢狀的,而 i 是不巢狀的,所以內層的 f 會隱藏掉外層的 f ,但是 i 不會相互隱藏。

2. 識別符號的名稱空間

        名稱空間是為了解決 “在相同作用域內如何區分 相同的識別符號”。 
        說明:①只有在相同作用域的情況下才能使用到名稱空間去區分識別符號,在巢狀的作用域不同的作用域區分識別符號都用不到名稱空間的概念。 
                  相同的作用域內,如果名稱空間不同,識別符號可以使用相同的名稱。否則,即如果名稱空間不同,編譯器會報錯,提示重複定義。

        按照C99(章節6.2.3),名稱空間可以分為四種:

            2.1  所有的標籤(label)都屬於同一個名稱空間。 
                    說明:①在同一個函式內,你的標籤不能相同。在同一個函式內,標籤可以和其他變數名稱相同。因為它們所屬的名稱空間不同。

            2.2  struct、enum和union的名稱,在C99中稱之為tag,所有的tag屬於同一個名稱空間。 
                   也就是說,如果你已經宣告struct A { int a }; 就不能在宣告 union A{ int a };

                   說明:之所以讓所有的tag組成一個名稱空間,由於Tag前面總是帶struct,enum或union關鍵字,所以編譯器可以將它們與其他的識別符號區分開。

           2.3  struct和union的成員屬於一個名稱空間,而且是相互獨立的。例如:如果你已經宣告struct A { int a }; 
                  其成員的名稱為a,你仍然可以宣告 struct B{ int a };或者union B{ int a };

                  說明:之所以讓struct和union的成員各自成為一個名稱空間,是因為它們的成員訪問時,需要通過 "."或"->"運算子,而不會單獨使用,所以編譯器可以將它們與其他的識別符號區分開。由於列舉型別enum的成員可以單獨使用,所以列舉型別的成員不在這一名稱空間內。

           2.4  其他所有的識別符號,屬於同一個名稱空間。包括變數名、函式名、函式引數,巨集定義typedef的型別名、enum的成員 等等。 
                  注意:如果識別符號出現重名的情況,巨集定義覆蓋所有其它識別符號,這是因為它在預處理階段而不是編譯階段處理。除了巨集定義之外其它類別的識別符號,處理規則是:內層作用域會隱藏掉外層作用域的識別符號。

舉例說明並分析

  1. ">#include <stdio.h>   
  2. #include <stdlib.h>  
  3.   
  4. int main(){  
  5.     struct A{   /* “結構體的tag”和“結構體成員”不在同一個名稱空間,所以名稱可以相同 */   
  6.         int A;  
  7.     };  
  8.     union B{  /* 根據第二條,這個union的tag不能是A,但是根據第三條,其成員的名稱可以與struct A的成員名稱相同 */  
  9.         int A;  
  10.     };  
  11.     struct A A; /* “結構體的tag”和“普通變數”不在同一個名稱空間,所以名稱可以相同 */   
  12.     union B B;  /* 上面的“結構體變數”和 這行的“聯合體變數”屬於同一個名稱空間,名稱不能相同,即不能是 union B A */  
  13.     int my_label = 1; /* “普通變數”和“標籤”不屬於同一個名稱空間,所以名稱可以相同 */  
  14.     A.A = 1;  
  15.     B.A = 20;  
  16.     printf("B.A == %d  /n/n", B.A);       
  17.       
  18.   my_label:     /* 這裡label 的名稱與上面變數的名稱 相同 */   
  19.     printf("A.A == %d  /n", A.A);  
  20.     A.A +=1;  
  21.     if(A.A <= 5){  
  22.         goto my_label;      
  23.     }  
  24.       
  25.     system("pause");  
  26.     return EXIT_SUCCESS;  
  27. }  

執行結果為:

  1. B.A == 20  
  2.   
  3. A.A == 1  
  4. A.A == 2  
  5. A.A == 3  
  6. A.A == 4  
  7. A.A == 5  

3. 識別符號的連結屬性

        主要用於處理多次宣告相同的識別符號名稱後,如何判斷這些識別符號是否是同一個。  
        原文對連結屬性(linkage)的定義如下:An identifier declared in different scopes or in the same scope more than once can be made to refer to the same object or function by a process called  linkage.

        注意:連結屬性(linkage)是相對於相同的識別符號名稱來說的,對於不同的識別符號,沒有連結屬性。

        按照C99(章節6.2.2),連結屬性分為三種:external(外部的), internal(內部的), none(無)。

型別

說明

預設(即不使用extern和static)

外部  external同一個識別符號,即使在不同的檔案中,也表示同一個實體。①具有檔案作用域變數函式。 
程式碼塊作用域內部的函式宣告
內部  internal同一個識別符號,僅僅在同一個檔案中才表示同一個實體。(如果不使用static,那麼預設沒有內部連結屬性的識別符號。只有被static修飾的具有檔案作用域的識別符號,才具有internal連結屬性)
無  none表示不同的實體所有其他的識別符號。如:函式的引數、程式碼塊作用域的變數、標籤等

        extern和static的使用:

        3.1  檔案作用域的變數和函式定義,即在所有 程式碼塊和引數列表之外的識別符號,使用static修飾,則具有 內部連結屬性。

        3.2  一個識別符號宣告為extern,並且前面已經對同一個識別符號進行了宣告,那麼 
              ①如果前一個宣告時internal或者external,那麼後一個宣告與前一個相同。(即儘管後一個使用了extern,但其連結屬性由前一個決定)。 
              ②如果前一個宣告為none,或者前一個宣告在當前作用域不可見,那麼這個識別符號的連結屬性為external。  
             舉例說明並分析:(注意所有檔案都在同一個工程中)

  1. /* 檔案《test1.c》 */  
  2. int a=1 ;   /* 這裡的a為external */   
  3. int b=1;    /* 這裡的b為external */   
  4.   
  5. void print_in_test1(){  
  6.     static int a;   /* 這裡是重新宣告一個變數a, 並且會隱藏掉外層的a。由於是static靜態型別,其預設初始化為0,所以下面的列印結果應為 0*/   
  7.     extern int b;   /* 雖然這裡將b用extern宣告,但是由於檔案前面宣告的b是external,所以b的連結屬性也沒有改變,依然是external,所以下面的列印結果應為 1 */   
  8.       
  9.     printf("test1.c:  a == %d  /n", a);  
  10.     printf("test1.c:  b == %d  /n", b);  
  11. }  
  12.   
  13.   
  14. /*檔案《test2.c》 */  
  15. static int a=2; /* 這裡的a為internal */   
  16.   
  17. void print_in_test2(){  
  18.     extern int a;  /* 雖然這裡將a用extern宣告,但是由於檔案前面宣告的a是internal,所以a的連結屬性並沒有改變,依然是internal */   
  19.     int b =2;  /* 這裡b為none,不會連結到test1.c中的 b,所以下面的列印結果應為 2 */   
  20.     printf("test2.c:  a == %d  /n", a);     /* 所以下面的列印結果應為 2 */   
  21.     printf("test2.c print_in_test2() :  b == %d  /n", b);  
  22. }  
  23.   
  24. void print2_in_test2(){  
  25.     extern int b;   /* b會連結到test1.c中的 b,而不是上面的函式中的 b,所以下面的列印結果應為 1 */  
  26.     printf("test2.c:  b == %d  /n", b);   
  27. }   
  28.   
  29. /* 檔案《main.c》 */  
  30. #include   
  31. #include   
  32.   
  33. extern int a; /* 會連結到test1.c中的 a,所以下面的列印結果應該為 1 */   
  34.   
  35. void print_in_test1();  /* 函式原型,會連結到test1.c中的 print_in_test1()*/  
  36.   
  37. int main(int argc, char *argv[])  
  38. {  
  39.   
  40.     void print_in_test2();  /* 函式原型,會連結到test2.c中的 print_in_test2()*/  
  41.     void print2_in_test2(); /* 函式原型,會連結到test2.c中的 print2_in_test2()*/  
  42.       
  43.     printf("main.c:  a == %d  /n", a);  
  44.     print_in_test1();  
  45.     print_in_test2();  
  46.     print2_in_test2();  
  47.       
  48.     system("PAUSE");      
  49.     return 0;  
  50. }  

        執行結果:

  1. main.c:  a == 1  
  2. test1.c:  a == 0  
  3. test1.c:  b == 1  
  4. test2.c:  a == 2  
  5. test2.c print_in_test2() :  b == 2  
  6. test2.c:  b == 1  

        3.3  如果不使用static和extern: 
                1.對於函式宣告:一定是external,無論是否在程式碼塊內部。 
                2.對於變數宣告:如果在   程式碼塊外,則是 external;否則是none

             例子可以參照上面的程式程式碼,《main.c》中宣告函式原型時,print_in_test1()在main函式外,print_in_test2()和print2_in_test2()在main函式內,雖然位置不同,但都是external的,都會正確連結到相應的函式。

4. 變數的生命週期、儲存型別

        變數的生存期(Storage durations),也就是變數的生命週期(lifetime),可以理解為:程式執行期間,變數從分配到地址 到 地址被釋放 這一過程。

        更具C99描述,變數的生存期分為三種型別:static(靜態), automatic(自動), and allocated(動態分配)。

        1.  屬於檔案作用域(即external或internal連結屬性)、以及被static修飾的變數,具有static靜態生存期

        2.  連結屬性為none,並且沒有static修飾 的變數,具有automatic自動生存期

        3.  allocated動態分配生存期,是指使用malloc函式,在程式的堆空間分配記憶體的變數。

        說明:

        4.1  生命週期、存數型別 都是針對變數,對於函式等其他識別符號沒有這個說法。 
               因為在程式執行期間,只有變數才需要分配記憶體和釋放記憶體,其他的諸如函式等都不需要。

        4.2  變數的生命週期儲存型別密切相關。

              ① 靜態生存期的變數儲存在靜態記憶體中。其中使用static修飾的變數,在C語言書籍中也被稱為“靜態變數”。靜態儲存的變數,在程式執行之前就已經建立,在程式整個執行期間一直存在,如果宣告時沒有被顯式的初始化,就會被自動初始化為0。          注意靜態變數當然是屬於靜態儲存方式,但是屬於靜態儲存方式的變數不一定就是靜態變數, 例如外部變數雖屬於靜態儲存方式,但不一定是靜態變數,必須由 static加以定義後才能成為靜態變數。 
              ② 自動生存期的變數儲存於棧或暫存器中。其中在程式碼塊內部宣告的變數,在C語言書籍中也被稱為“自動變數”,使用auto修飾符,預設可以省略。對於自動儲存的變數當程式執行到含有自動變數的程式碼段時,自動變數才被建立,並且不會被自動初始化,程式碼段執行結束,自動變數就自動銷燬,釋放掉記憶體。如果程式碼段被反覆執行,那麼自動變數就會反覆被建立和銷燬。注意這一點和靜態變數不同,靜態變數只建立一次,到程式結束才銷燬。

              ③  動態分配生存期的變數儲存於中,也不會被自動初始化,使用free函式釋放記憶體。

        4.3  修改變數的儲存型別(如用static將自動變數變為靜態變數),並不會修改變數的作用域,變數的作用域仍然有其宣告的位置決定。

        4.4  變數的儲存型別修飾符一共有五個:static、auto、register、extern、typedef。

        4.5  函式的形式引數,如果使用修飾符,只能使用register修飾,表示執行時引數儲存在暫存器上。注意:形式引數是不能用auto修飾的。

5. 總結

        下圖為一個變數宣告,在 不同的作用域 對應的其他屬性:

作用域

宣告位置

連結屬性

儲存型別

預設初始化值

使用static修飾

file在所有“程式碼塊”和“引數列表”之外externalstatic  0internal
block在“程式碼塊”或者“函式的引數列表”內部noneautomatic形式引數 呼叫時被初始化;程式碼塊內部的不自動初始化none
function函式體內-----------------標籤,不需要初始化---------
function prototype宣告的函式原型的引數列表中(注意與“函式定義”不同)-----------------不需要初始化---------

 


 


轉載於:https://www.cnblogs.com/wangfengju/p/6173005.html

相關文章