無論學習哪一種語言,都免不了要討論這些問題。而且這些問題,深究起來有時也讓我們很迷惑。
識別符號的定義無需多講,只需注意不僅僅是指變數,還有函式,標籤等。
1. 識別符號的作用域
作用域是指允許對識別符號進行訪問的位置範圍。按照C99(章節6.2.1),C語言的作用域共有 4 種型別:檔案作用域、程式碼塊作用域、函式作用域、函式原型作用域。
型別 | 位置 | 說明 |
檔案作用域 (file) | 在所有 程式碼塊和引數列表 之外 | 整個檔案內都可以訪問 |
程式碼塊作用域 ( block) | 在“程式碼塊”或者“函式的引數列表”內部 | 只有所在的程式碼塊內可以訪問 |
函式作用域 (function) | 函式體內 | 具有此作用域的只有一種語句:只有goto語句要使用的“語句標籤”。簡化為一條規則:一個函式中的語句標籤(即label)不可相同。 |
函式原型作用域 (function prototype) | 宣告的函式原型的引數列表中(注意與“函式定義”不同) | 由於函式原型的引數名稱可以省略,即使不省略,也不要求和“函式定義”中的形參列表中名稱相同。 只有一種情況會發生衝突:引數列表中的有重複的變數名。(這時編譯報錯: redefinition of parameter ) |
說明:當出現兩個識別符號名稱相同的情況,而且都屬於同一個名稱空間,那麼在內層程式碼塊,內層的那個識別符號會隱藏外層的那個識別符號。
舉例說明並分析:
- int my_func(int a, int b); /* myfunc是“檔案作用域”;a,b是 “函式原型作用域” */
- int a;/* a是檔案作用域。 注意:雖然上面的函式原型中將引數名稱宣告為a, 但是由於作用域不同,是合法的。下一行的b也是這種情況 */
- static int b; /* b是檔案作用域 */
- int d( int n ){ /* d是“檔案作用域”。因為這是函式定義,而不是函式原型,所以形式引數n 是“程式碼塊作用域” */
- /* 由於形式引數中已經宣告n,那麼在函式體內的最外層變數的名稱就不能再為n,因為同一個作用域內不允許對同一個變數進行多次宣告。
- 如果宣告,編譯器會提示重複宣告變數。(在某些較老版本的編譯器是允許的,但是C99標準是不允許的)
- 在不同的作用域內可以 */
- int f; /* f是程式碼塊作用域 */
- int g(int k); /* 函式原型,位於函式體程式碼塊內。宣告的函式名稱g是“程式碼塊作用域”,引數k是“函式原型作用域” */
- my_label: /* 定義一個label,是“函式作用域” */
- ... /* 下面的程式碼塊可以是while迴圈、for迴圈或if語言等等*/
- {
- int f, g, i; /* 都是程式碼塊作用域,而且只是在內層程式碼塊,在外層程式碼塊不可見 */
- /* 對於f,外層已經存在f,這裡會隱藏掉外層的f,即在這個內層程式碼塊中無法訪問外層的f */
- int n; /* 程式碼塊作用域,由於這裡已經不是函式體內的最外層,所以可以宣告與函式的形式引數同名的變數,
- 同樣會隱藏掉外層的變數n */
- }
- ... /* 另外一個 程式碼塊 */
- {
- int i; /* 程式碼塊作用域,雖然上面的一個內層程式碼塊中已經存在i,但是由於這兩個程式碼塊不存在巢狀關係,所以也不存在隱藏現象 */
- }
- }
注意事項:
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的
成員 等等。
注意:如果識別符號出現重名的情況,巨集定義覆蓋所有其它識別符號,這是因為它在預處理階段而不是編譯階段處理。除了巨集定義之外其它類別的識別符號,處理規則是:內層作用域會隱藏掉外層作用域的識別符號。
舉例說明並分析:
- ">#include <stdio.h>
- #include <stdlib.h>
- int main(){
- struct A{ /* “結構體的tag”和“結構體成員”不在同一個名稱空間,所以名稱可以相同 */
- int A;
- };
- union B{ /* 根據第二條,這個union的tag不能是A,但是根據第三條,其成員的名稱可以與struct A的成員名稱相同 */
- int A;
- };
- struct A A; /* “結構體的tag”和“普通變數”不在同一個名稱空間,所以名稱可以相同 */
- union B B; /* 上面的“結構體變數”和 這行的“聯合體變數”屬於同一個名稱空間,名稱不能相同,即不能是 union B A */
- int my_label = 1; /* “普通變數”和“標籤”不屬於同一個名稱空間,所以名稱可以相同 */
- A.A = 1;
- B.A = 20;
- printf("B.A == %d /n/n", B.A);
- my_label: /* 這裡label 的名稱與上面變數的名稱 相同 */
- printf("A.A == %d /n", A.A);
- A.A +=1;
- if(A.A <= 5){
- goto my_label;
- }
- system("pause");
- return EXIT_SUCCESS;
- }
執行結果為:
- B.A == 20
- A.A == 1
- A.A == 2
- A.A == 3
- A.A == 4
- 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。
舉例說明並分析:(注意所有檔案都在同一個工程中)
- /* 檔案《test1.c》 */
- int a=1 ; /* 這裡的a為external */
- int b=1; /* 這裡的b為external */
- void print_in_test1(){
- static int a; /* 這裡是重新宣告一個變數a, 並且會隱藏掉外層的a。由於是static靜態型別,其預設初始化為0,所以下面的列印結果應為 0*/
- extern int b; /* 雖然這裡將b用extern宣告,但是由於檔案前面宣告的b是external,所以b的連結屬性也沒有改變,依然是external,所以下面的列印結果應為 1 */
- printf("test1.c: a == %d /n", a);
- printf("test1.c: b == %d /n", b);
- }
- /*檔案《test2.c》 */
- static int a=2; /* 這裡的a為internal */
- void print_in_test2(){
- extern int a; /* 雖然這裡將a用extern宣告,但是由於檔案前面宣告的a是internal,所以a的連結屬性並沒有改變,依然是internal */
- int b =2; /* 這裡b為none,不會連結到test1.c中的 b,所以下面的列印結果應為 2 */
- printf("test2.c: a == %d /n", a); /* 所以下面的列印結果應為 2 */
- printf("test2.c print_in_test2() : b == %d /n", b);
- }
- void print2_in_test2(){
- extern int b; /* b會連結到test1.c中的 b,而不是上面的函式中的 b,所以下面的列印結果應為 1 */
- printf("test2.c: b == %d /n", b);
- }
- /* 檔案《main.c》 */
- #include
- #include
- extern int a; /* 會連結到test1.c中的 a,所以下面的列印結果應該為 1 */
- void print_in_test1(); /* 函式原型,會連結到test1.c中的 print_in_test1()*/
- int main(int argc, char *argv[])
- {
- void print_in_test2(); /* 函式原型,會連結到test2.c中的 print_in_test2()*/
- void print2_in_test2(); /* 函式原型,會連結到test2.c中的 print2_in_test2()*/
- printf("main.c: a == %d /n", a);
- print_in_test1();
- print_in_test2();
- print2_in_test2();
- system("PAUSE");
- return 0;
- }
執行結果:
- main.c: a == 1
- test1.c: a == 0
- test1.c: b == 1
- test2.c: a == 2
- test2.c print_in_test2() : b == 2
- 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 | 在所有“程式碼塊”和“引數列表”之外 | external | static | 0 | internal |
block | 在“程式碼塊”或者“函式的引數列表”內部 | none | automatic | 形式引數 呼叫時被初始化;程式碼塊內部的不自動初始化 | none |
function | 函式體內 | --------- | -------- | 標籤,不需要初始化 | --------- |
function prototype | 宣告的函式原型的引數列表中(注意與“函式定義”不同) | --------- | -------- | 不需要初始化 | --------- |