5. C語言變量的存儲類別和生存期
我們知道,變量是有數據類型的,用以說明它占用多大的內存空間,可以進行什么樣的操作。
除了數據類型,變量還有一個屬性,稱為“存儲類別”。存儲類別就是數據在內存中的存放區域。一個正在運行的C程序的內存空間可以分為五個區域:程序代碼區、靜態數據區、堆區、棧區和命令行參數區,其中靜態數據區和棧區可以用來存放變量的值。
靜態數據區的內存在程序啟動時就已經由操作系統分配好,占用的空間固定,程序運行期間不再改變,程序運行結束后才由操作系統釋放;它可以存放全局變量、靜態變量、一般常量和字符串常量。
棧區的內存在程序運行期間由操作系統根據需要來分配(使用到變量才分配內存;如果定義了變量但沒有執行到該代碼,也不會分配內存),占用的空間實時改變,使用完畢后立即釋放,不必等到程序運行結束;它可以存放局部變量、函數參數等。
可以通過C語言中的關鍵字來控制變量的存放區域;C語言共有 4 個關鍵字用來指明變量的存儲類別:auto(自動的)、static(靜態的)、register(寄存器的)、extern(外部的)。
知道了變量的存儲類別,就可以知道變量的生存期。通俗地講,生存期指的是在程序運行過程中,變量從創建到銷毀的一段時間,生存期的長短取決于變量的存儲類別,也就是它所在的內存區域。
auto 變量
auto 是自動或默認的意思,很少用到,因為所有的變量默認就是 auto 的。也就是說,定義變量時加不加 auto 都一樣,所以一般把它省略,不必多此一舉。
例如:
- int n = 10;
與
- auto int n = 10;
的效果完全一樣。
6. C語言extern變量和函數
在所有的代碼塊(函數、if 塊、switch 塊等)之外定義的變量稱為全局變量,它的作用范圍默認是整個程序,也就是所有的源文件,包括 .c 和 .h 文件。
如果你一直在編寫單個 .c 文件的程序,那么請注意,全局變量的作用范圍不是從變量定義處到該文件結束,在其他文件中也有效。
雖然全局變量的作用范圍是整個程序,但是如果希望在 a.c 中使用 b.c 中的變量,也必須先進行聲明。聲明使用 extern 關鍵字,請看下面的代碼。
a.c 源碼:
1 #include <stdio.h> 2 #include <stdlib.h> 3 extern int num; // 必須對 num 進行聲明 4 int main () 5 { 6 printf("num = %d", num); 7 system("pause"); 8 return 0; 9 }
b.c 源碼:
1 int num = 100; // 對 num 進行定義
運行結果:
num = 100
我們在 b.c 中定義了一個全局變量 num,在 a.c 中調用了它。extern int num; 的作用是告訴編譯器 num 不在 a.c 中,請到其他文件中查找。如果沒有 extern,編譯器就會在當前文件中查找,發現沒有就會報錯。
提示:編譯是針對單個源文件的,在編譯 a.c 時編譯器找不到 num,鏈接時才會在 b.c 的目標代碼(.obj 文件)中找到 num。
與其他變量不同,extern 變量有聲明和定義之分。
extern 變量的定義格式為:
- extern type name = value;
不過 extern 可以省略(我們通常就是這么做的),全局變量默認就是 extern 的,如 b.c 文件所示。
聲明格式為:
- extern type name;
注意:
- 在定義 extern 變量時不能省略 value,否則就變成了變量聲明。
- 聲明 extern 變量時要指明數據類型(必須和定義時的數據類型一致)。
- 聲明可以有多次,定義只能有一次。
在 a.c 中,我們在所有代碼塊外部對 num 進行了聲明,這個時候 num 的作用范圍是 a.c 整個文件(確切的說是從聲明開始處到文件結束)。如果在代碼塊內部聲明會怎樣呢?
對 a.c 進行更改:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main () 4 { 5 { 6 extern int num; 7 printf("num = %d", num); 8 } 9 printf("num = %d", num); 10 system("pause"); 11 return 0; 12 }
編譯時報錯,第 9 行的 num 未聲明。這說明 extern 變量的作用域跟它的聲明位置有關,在代碼塊內聲明的 extern 變量在代碼塊外無效。
extern 函數
從本質上講,函數和變量是類似的,它們都指向內存中的一塊區域:函數指向存放了函數體二進制代碼的程序代碼區,變量指向靜態數據區、棧區或堆區。
extern 除了用于變量,也可以用于函數,請看下面的代碼:
sum.c 源碼:
1 int sum(int n1, int n2) 2 { 3 return n1 + n2; 4 }
main.c 源碼:
1 #include <stdio.h> 2 #include <stdlib.h> 3 extern int sum(int, int); 4 int main () 5 { 6 int num1 = 20, num2 = 110; 7 printf("%d + %d = %d", num1, num2, sum(num1, num2)); 8 system("pause"); 9 return 0; 10 }
運行結果:
20 + 110 = 130
我們在 sum.c 中定義了一個函數 sum() 用來計算兩個數的和,在 main.c 中對函數進行了調用。extern 的作用是告訴編譯器 sum() 函數不在 main.c 中,請到其他文件中去查找。
但是,函數和變量的聲明有所不同,對于函數,你可以省略 extern。例如將 main.c 中的:
- extern int sum(int, int);
改為:
- int sum(int, int);
仍然能夠編譯通過并正確運行。
這是因為,函數的定義和聲明區別很明顯,有函數體就是定義,沒有函數體就是聲明,所以有沒有 extern 都是函數聲明。但是變量不一樣,沒有 extern 就是變量定義,重復定義是錯誤的。
?
?7.?C語言static變量和函數
上一節我們講到,全局變量和函數的作用范圍默認是整個程序,也就是所有的源文件。這給我們帶來了很大的方便,讓我們能夠在 A 文件中調用 B 文件中定義的變量和函數,不必把所有的代碼都集中到一個文件,有利于模塊化的程序設計。
但是有時候這也會帶來沖突,例如在 a.c 中定義了一個全局變量 n,在 b.c 中又定義了一次,編譯時就會發生重復定義的錯誤,因為變量只能定義一次。
如果兩個文件都是我們自己編寫的或者其中一個是,遇到這樣的情況還比較好處理,改變變量的名字就可以;但如果兩個文件都是其他程序員編寫的,或者是第三方的庫,修改起來就頗費精力了。所以,實際開發中我們一般將不需要被其他文件調用的全局變量或函數的作用范圍限制在當前文件中。
可以通過 static 關鍵字來限制,請看下面的代碼。
a.c 源碼:
1 #include <stdio.h> 2 static int n = 10; 3 void print_n_a() 4 { 5 printf("n(a.c) = %d\n", n); 6 }
b.c 源碼:
1 #include <stdio.h> 2 static int n = 20; 3 void print_n_b() 4 { 5 printf("n(b.c) = %d\n", n); 6 }
main.c 源碼:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int n = 100; 4 int main () 5 { 6 print_n_a(); 7 print_n_b(); 8 printf("n(main.c) = %d\n", n); 9 system("pause"); 10 return 0; 11 }
運行結果:
n(a.c) = 10
n(b.c) = 20
n(main.c) = 100
我們在 a.c、b.c 和 main.c 中都定義了變量 n,a.c 和 b.c 中的變量 n 都只在各自的文件內有效,main.c 中的變量 n 在整個程序內有效。
由此可見,加了 static 的變量或函數的作用范圍僅限于當前文件,對其他源文件隱藏,利用這一特性可以在不同的文件中定義同名的變量或函數,而不必擔心命名沖突。
static 局部變量
static 聲明的變量稱為靜態變量,不管是全局變量還是局部變量,都存儲在靜態數據區(全局變量本來就存儲在靜態數據區,即使不加 static)。
靜態數據區的數據在程序啟動時就會初始化,直到程序運行結束;對于代碼塊中的靜態局部變量,即使代碼塊執行結束,也不會銷毀。
注意:靜態數據區的變量只能初始化(定義)一次,以后只能改變它的值,不能再被初始化,即使有這樣的語句,也無效。
請看下面的代碼:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main () 4 { 5 int result, i; 6 for(i = 1; i<=100; i++) 7 { 8 result = sum(i); 9 } 10 printf("1+2+3+...+99+100 = %d\n", result); 11 system("pause"); 12 return 0; 13 } 14 int sum(int n) 15 { 16 // 也可以不賦初值 0,靜態數據區的變量默認初始化為 0 17 static int result = 0; 18 result += n; 19 return result; 20 }
運行結果:
1+2+3+...+99+100 = 5050
我們在 sum() 中定義了一個靜態局部變量 result,它存儲在靜態數據區,sum() 函數執行結束也不會銷毀,下次調用繼續有效。靜態數據區的變量只能初始化一次,第一次調用 sum() 時已經對 result 進行了初始化,所以再次調用時就不會初始化了,也就是說 static int result = 0; 語句無效。
靜態局部變量雖然存儲在靜態數據區,但是它的作用域僅限于定義它的代碼塊,sum() 中的 result 在函數外無效,與 main() 中的 result 不沖突,除了變量名一樣,沒有任何關系。
總結起來,static 變量的主要作用有兩個。
1) 隱藏
程序有多個源文件時,將全局變量或函數的作用范圍限制在當前文件,對其他文件隱藏。
2) 保持變量內容的持久化
將局部變量存儲到靜態數據區。靜態數據區的內存在程序啟動時就已分配好(內存中所有的字節默認值都是0x00),直到程序運行結束。
?
8. C語言register變量
一般情況下,變量的值是存儲在內存中的,CPU 每次使用數據都要從內存中讀取。如果有一些變量使用非常頻繁,從內存中讀取就會消耗很多時間,例如 for 循環中的增量控制:
1 int i; 2 for(i=0; i<1000; i++) 3 { 4 // Some Code 5 }
執行這段代碼,CPU 為了獲得 i,會讀取 1000 次內存。
為了解決這個問題,可以將使用頻繁的變量放在CPU的通用寄存器中,這樣使用該變量時就不必訪問內存,直接從寄存器中讀取,大大提高程序的運行效率。
寄存器、緩存、內存
為了加深對 register 變量的理解,這里有必要講一下CPU寄存器。
按照與CPU的遠近來分,離CPU最近的是寄存器,然后是緩存,最后是內存。
寄存器是最貼近CPU的,而且CPU只在寄存器中進行存取。寄存的意思是暫時存放數據,不用每次都從內存中取,它是一個臨時的存放數據的空間。
而寄存器的數據又來源于內存,于是 CPU <-- 寄存器 <-- 內存,這就是它們之間的信息交換。
那么為什么還需要緩存呢?因為如果頻繁地操作內存中同一地址上的數據會影響速度,于是就在寄存器和內存之間設置一個緩存,把使用頻繁的數據暫時保存到緩存,如果寄存器需要讀取內存中同一地址上的數據,就不用大老遠地再去訪問內存,直接從緩存中讀取即可。
緩存的速度遠高于內存,價格也是如此。
注意:緩存的容量是有限的,寄存器只能從緩存中讀取到部分數據,對于使用不是很頻繁的數據,會繞過緩存,直接到內存中讀取。所以不是每次都能從緩存中得到數據,這就是緩存的命中率,能夠從緩存中讀取就命中,否則就沒命中。
關于緩存的命中率又是一門學問,哪些數據保留在緩存,哪些數據不保留,都有復雜的算法。

注意:上面所說的CPU是指CPU核心,從市場上購買的CPU已是封裝好的套件,附帶了寄存器和緩存,插到主板上就可以用。
從經濟和速度的綜合考慮,緩存又被分為一級緩存、二級緩存和三級緩存,它們的存取速度和價格依次降低,容量依次增加。購買到的CPU一般會標出三級緩存的容量。
register 變量
寄存器的數量是有限的,通常是把使用最頻繁的變量定義為 register 的。
來看一個計算 π 的近似值的例子,求解的一個近似公式如下:
為了提高精度,循環的次數越多越好,可以將循環的增量控制定義為寄存器變量,如下所示:
1 #include <stdio.h> 2 #include <conio.h> 3 int main() 4 { 5 register int i = 0; // 寄存器變量 6 double sign = 1.0, res = 0, ad = 1.0; 7 for(i=1; i<=100000000; i++) 8 { 9 res += ad; 10 sign=-sign; 11 ad=sign/(2*i+1); 12 } 13 res *= 4; 14 printf("pi is %f", res); 15 getch(); 16 return 0; 17 }
運行結果:
pi is 3.141593
關于寄存器變量有以下事項需要注意:
1) 為寄存器變量分配寄存器是動態完成的,因此,只有局部變量和形式參數才能定義為寄存器變量。
2) 局部靜態變量不能定義為寄存器變量,因為一個變量只能聲明為一種存儲類別。
3) 寄存器的長度一般和機器的字長一致,所以,只有較短的類型如int、char、short等才適合定義為寄存器變量,諸如double等較大的類型,不推薦將其定義為寄存器類型。
4) CPU的寄存器數目有限,因此,即使定義了寄存器變量,編譯器可能并不真正為其分配寄存器,而是將其當做普通的auto變量來對待,為其分配棧內存。當然,有些優秀的編譯器,能自動識別使用頻繁的變量,如循環控制變量等,在有可用的寄存器時,即使沒有使用 register 關鍵字,也自動為其分配寄存器,無須由程序員來指定。
?