1. 程式人生 > >C的變數型別、作用域與生命週期的總結

C的變數型別、作用域與生命週期的總結

# C的變數型別、作用域與生命週期的總結 最近在看“C Programing Language" (Kernighan, Ritchie)關於外部變數的討論,之前在學C的時候對這些extern, auto, static, register等不是太理解,這本書講的很詳細,現在總結一下。 首先, C的變數分成區域性變數 local variable 和全域性變數 global variable。 【注】 1. C 中區域性(`local`)變數(也有翻譯成本地變數),也可以叫做內部(`internal`)變數 2. C 中全域性(`global`)變數,又可叫外部(`external`)變數。 這些稱呼都可以互換,不同的稱呼可能強調的是不同的特性,以下儘量用對應使用的關鍵字來稱呼,比如區域性變數將用自動變數(`auto`)來稱呼,全域性變數將用外部變數(`extern`)來稱呼。 C語言程式可以看成是由許多外部物件構成的,這些外部物件可以是變數或函式。外部(`external`)和內部(`internal`)是相對的,`internal`是用來描述在函式內部的函式引數或變數,`external`描述的是定義在函式外部的變數。由於C語言不允許在函式內部定義函式,因此函式都可以看成是外部的(`extern`)。 栗子: ```c #include #define MAX 100 char s[MAX]; void printString(void) { printf("%s", s); } int main(void) { scanf("%s", s); printString(s); return 0; } ``` 這個簡單的`printString.c`原始檔即可看成是由三個外部物件構成:外部變數`s`, 外部函式`printString()`和`main()`組成,還有一個`include`的標準庫檔案`stdio.h`(這個下次再談)。 預設情況下,外部變數與函式具有以下性質:通過同一個名字引用的所有外部變數(即使這種引用來自單獨編譯的不同函式)實際上都是引用記憶體中同一個物件。 因為外部變數可以在全域性範圍內訪問,這就為函式之間的資料交換提供一種新的方式,可以代替函式引數與返回值,如上一個栗子。任何函式都可以通過名字訪問一個外部變數,當然這個名字需要通過某種方式來宣告。 ## 外部變數與自動變數的作用域與生命週期 - 變數或者符號的作用域是指程式中可以使用這個變數或名字的範圍。這個可以看成是靜態的程式碼範圍 - 變數生命週期則是指該變數存在的時間範圍。這個可以理解為程式執行時的變數存在的時間週期。 自動變數只能在函式內部使用,作用域從宣告處開始直至函式結束,生命週期是從其所在的函式被呼叫時,變數開始存在,在函式退出時變數將消失。對於在函式開頭宣告的自動變數,其作用域即為宣告該變數名的函式內部,函式的引數也是如此,實際上可以將它看作是這個函式的區域性變數。當然,自動變數也可以定義在函式內的語句塊中,比如在下面的for迴圈中定義的臨時變數temp: ```c int func(void) { int i; for(i = 0; i < 10; i++) { int temp; ... } } ``` 這種變數當然作用域是限定在語句塊內部,生命週期也是在該語句塊內部,當程式執行完該語句塊,變數也就消失了。 外部變數作用域為從其定義處開始直至所在的檔案的結尾結束,生命週期是永久存在的,即程式執行期間一直存在,它們的值在一次函式呼叫結束到下一次函式呼叫開始之前保持不變。另一方面,可以通過新增宣告的方式來使用外部變數,按照上面所說,外部變數是唯一的,因此新增`extern`宣告可以擴充套件外部變數的作用域到其他檔案中。 如果要在外部變數的定義之前使用該變數,或者外部變數的定義與變數的使用不在同一個檔案中,則必須在相應的變數宣告中強制使用關鍵字`extern`。 將外部變數的定義與宣告區別開是很有必要的,外部變數的宣告用於說明變數的屬性(主要是型別),而外部變數的定義除此之外還會引起記憶體的分配(在定義後編譯程式將為它分配記憶體單元)。 而自動變數則不然,自動變數在C中沒有定義這一說法,只要先宣告再使用即可,這是因為自動變數(即區域性變數)是在執行時由棧來管理的,而外部變數(即全域性變數)是在編譯過程中由編譯器、彙編器分配儲存地址,一直到連結時確定記憶體位置(這些內容將在之後會專門總結有關編譯、彙編、連結的內容),在這裡都可以理解為在編譯時即分配了記憶體單元。 栗子:如果將下面這兩條語句放在所有函式的外部: ```c int a; double b[MAX]; ``` 則這兩條語句將定義外部變數a與b,併為之分配記憶體,同時這兩條語句還可以作為該原始檔中其餘部分的宣告。而下面的兩行語句: ```c extern int a; extern double b[]; ``` 為所在檔案該語句之後的部分聲明瞭一個`int`型別的外部變數a以及一個`double`型別的外部變數b(該陣列的長度在其他地方確定),但這兩個宣告並沒有建立變數或為他們分配記憶體。 在程式的原始檔中,一個外部變數只能在某個檔案中定義一次,而其他檔案可以通過`extern`宣告來訪問它(定義外部變數的原始檔中也可以包含對該外部變數的`extern`宣告)。外部變數的定義必須指定陣列的長度,但extern宣告則不一定要指定陣列的長度。外部變數的初始化只能出現在其定義中(注:若外部變數未初始化,編譯器將它初始化為0)。 【注】外部變數的宣告也可以通過上下文隱式宣告(即如上所說,定義即可作為之後語句的宣告)如下面程式版本2中的外部變數`len`和`buf`在main函式中就無需在main中再宣告。 版本1: ```c #include #define MAXLENGTH 1000 // buffer最大長度 char buf[MAXLENGTH]; int getline(void); /*一個簡單的copy-paste程式 */ int main(void) { while (getline()!=EOF) { printf("%s\n", buf); } return 0; } int getline(void) { int c, i; extern char buf[MAXLENGTH]; i= 0; while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n') buf[i++] = c; if (c = '\n') buf[i++] = c; buf[i] = '\0'; return i; } ``` 版本2: ```c #include #define MAXLENGTH 1000 // buffer最大長度 char buf[MAXLENGTH]; int getline(void); /*一個簡單的copy-paste程式 */ int main(void) { while (getline()!=EOF) { printf("%s\n", buf); } return 0; } int getline(void) { int c, i; // 通過上下文隱式宣告 buf[] i= 0; while ( i < MAXLENGTH && (c = getchar()) != EOF && c != '\n') buf[i++] = c; if (c = '\n') buf[i++] = c; buf[i] = '\0'; return i; } ``` ## 靜態變數與暫存器變數 ### 靜態變數 之前已經提到了,外部變數與自動變數,其中外部變數是可以被全域性使用的,這個全域性指的是整個源程式的所有原始檔都可以通過新增`extern`宣告來使用。但是,如果我們希望限定這個外部變數僅限於該定義的原始檔使用,而不希望被其他原始檔使用。那我們可以使用static宣告限定外部變數和函式,可以將其宣告的物件的作用域限定為該原始檔的剩餘部分。通過static限定外部物件,可以達到隱藏外部物件的目的。 ```c static char buf[BUFSIZE]; static int bufp = 0; ``` 變數宣告為static之後,該變數即為靜態儲存,其他檔案中的函式就不可以訪問變數`buf`, `bufp`,因此這兩個名字就不會和同一程式中的其他檔案中相同名字的變數相沖突。 【注】:多個函式中的自動變數同名,也不會造成衝突,因為在編譯過程中,編譯器會將自動變數改成不同名字,比如加上函式名,具體做法依賴於編譯器版本。 外部的`static`宣告多用於變數,當然,也可以用於宣告函式。通常情況下,函式名是全域性可訪問的,對整個程式的各個部分都是可見的。但是,如果把函式宣告為`static`型別,則該函式除了對該函式宣告所在的檔案可見外,其他檔案都無法訪問。 `static`也可用於宣告自動變數,`static`型別的自動變數同一般的自動變數一樣,是某個特定函式內的區域性變數,只能在該函式中使用。但它與一般的自動變數不同的是,不管其所在函式是否被呼叫,它一直存在。換句話說,`static`型別的內部變數作用域不變,生命週期和外部變數一樣為整個程式執行期間。 ### 暫存器變數 `register`宣告告訴編譯器,它所宣告的變數在程式中使用頻率較高。其思想史,將`register`變數放在機器的暫存器中,這樣可以使程式更小,執行速度更快。 `register`宣告的形式如下: ```c register int x; register char c; ``` `register`宣告只適用於自動變數以及函式的形式引數,看下面的例子: ```c void f(register unsigned a, register long n) { register int i; ... } ``` 實際使用的時候,底層硬體環境會對暫存器變數的使用有一些限制。每個函式中只有很少的變數可以儲存在暫存器中,且只允許某些型別的變數。但是,過量的暫存器變數並沒有什麼害處,因為編譯器可以忽略過量的或不支援的暫存器變數宣告。另外,無論暫存器變數實際上是不是存放在暫存器中,它的地址都是不能訪問的。 ## 總結 | 變數型別 | 定義 | 作用域 | 生命週期 | 說明 | | ---------- | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 自動變數 | 定義在函式內部或者是函式引數 | 自宣告其至函式結尾或者是所在語句塊結尾 | 作用域生效則生效,作用域失效則失效,多次呼叫,重新建立該變數 | 在執行時,由棧管理 | | 外部變數 | 定義在函式外部或者是函式 | 自定義起至所在檔案結尾,可通過`extern`宣告,擴充套件至全域性 | 整個程式執行期間 | 在編譯時,一旦定義即建立變數、分配記憶體 | | 靜態變數 | 宣告時使用,通過新增`static`來宣告 | 宣告外部變數時,該外部變數作用域僅為宣告所在檔案,宣告自動變數時,不改變 | 整個程式執行期間 | 可以限定全域性變數,函式或者是自動變數 | | 暫存器型別 | 通過新增`register`宣告 | - | - | 只能限定自動變數或函式引數,可能被存放在暫存器中,也可能被忽略,但是被宣告為暫存器型別的變數地址不可訪問 | 因此,可以將變數分為:被初始化的全域性範圍的外部變數,被初始化的靜態型別外部變數,未被初始化的兩類外部變數,自動變數,靜態型別自動變數,暫存器型別自動變數。 至於為什麼這麼分,下次討論編譯的時候用