C 存儲類別
C提供了多種不同的模型或存儲類別(storage class)在內存中儲存數據。要理解這些存儲類別,先要複習一些概念和術語。標識符是C語言中用於標識唯一對象的符號,包括變量名、函數名、命令名稱或常量名稱等。
- 作用域(scope)
- 鏈接(linkage)
- 存儲期(storage duration)
作用域(scope)
作用域描述程序中可訪問標識符的區域,也就是說在程序的哪些部分可以使用該標識符
在C語言中,共有三種作用域
- 塊作用域(block scope):塊是用一對花括號括起來的代碼區域。定義在塊中的變量具有塊作用域,塊作用域變量的可見範圍是從定義處到包含該定義的塊的末尾。另外,函數的型材也具有塊作用域。
- 函數原型作用域(function prototype scope:用於函數原型中的形參名(變量名),函數原型作用域的範圍是從形參定義處到原型聲明結束。
- 文件作用域(file scope):定義在函數外面的變量具有文件作用域,具有文件作用域的變量從它的定義處到該定義所在文件的末尾均可見。
鏈接(linkage)
恩鏈接的主要作用是定義在程序具有多個文件時,是否可以跨文件訪問。作用域和鏈接描述了標識符的可見性。
C語言共有三種鏈接屬性
- 外部鏈接(external linkage):外部鏈接變量可以在多文件程序中使用
- 內部鏈接(internal linkage):內部鏈接變量只能在一個翻譯單元中使用
- 無鏈接(no linkage):無鏈接變量只能被定義塊所私有,不能被程序其他部分引用。
具有塊作用域、函數作用域或函數原型作用域的變量都是無鏈接變量,意味着這些變量屬於定義它們的塊、函數或原型私有。
具有文件作用域的變量可以是外部鏈接或內部鏈接,通過關鍵字static
定義
int giants = 5; // 文件作用域,外部鏈接
static int dodgers = 3; // 文件作用域,內部鏈接
int main()
{
...
}
...
使用static
修飾的變量,具有內部鏈接屬性。具有文件作用域的變量默認是外部鏈接。
對於變量dodgers
和變量giants
, 該文件中的任意函數都可使用它。變量dodgers
屬文件私有,對於其他文件不可見,而對於變量giants
, 該文件和同一程序的其他文件都可以使用,只需要在引用這個變量的外部文件中聲明
extern int giants;
上述代碼中的extern
關鍵字告訴編譯器,這不是一個定義,只是一個聲明,表示這個變量位於其他文件中。不要用extern
進行外部定義,它只是用來引用一個已經存在的外部定義。
存儲期(storage duration)
存儲期描述了通過這些標識符訪問的對象的生存期,即變量在內存中的生存時間。
C對象共有四種存儲期
- 靜態存儲期(static storage duration):在程序執行期間一直存在,文件作用域變量具有靜態存儲期。注意,對於文件作用域變量,關鍵字
static
表明了其鏈接屬性,而非存儲期。以static
聲明的文件作用域具有內部鏈接。但無論內部鏈接還是外部鏈接,所有的文件作用域變量都具有靜態存儲期。另外關鍵字static
還可以使得塊作用域變量具有靜態存儲期。 - 線程存儲期(thread storage duration):用於併發設計,程序執行可被分爲多個線程。具有線程存儲期的對象,從被聲明到線程結束一直存在。以關鍵字
_Thread_local
聲明一個對象時,每個線程都獲得該變量的私有備份。 - 自動存儲期(automatic storage duration):塊作用域的變量通常具有自動存儲期,當程序進入定義變量的塊時,爲這些變量分配內存;當退出這個塊時,釋放剛纔爲變量分配的內存。變長數組稍有不同,他們的存儲期從聲明處到塊的末尾,而不是從塊的開始處到塊的末尾
- 動態分配存儲期(allocated storage duration):用動態內存分配函數分配和解分配。比如
malloc
函數,從聲明到free
釋放內存爲止。
C語言中,變量的屬性信息不僅僅是其類型,還包括了作用域、鏈接和存儲期。這些不同屬性的組合方式呈現了變量的不同表現,下面開始討論存儲類別。
存儲類別(Storage Class)
變量的存儲類別取決於作用域
、鏈接
和存儲期
。存儲類別由聲明變量的位置和與之關聯的關鍵字決定,定義在所有函數外部的變量具有文件作用域、外部鏈接、靜態存儲期。聲明在函數中的變量是自動變量,除非該變量前面使用了其他關鍵字。他們具有塊作用域、無鏈接、自動存儲期。以static
關鍵字聲明在函數中的變量具有塊作用域、無鏈接、靜態存儲期。以static
關鍵字聲明在函數外部的變量具有文件作用域、內部鏈接、靜態存儲期。
C11新增了一個存儲類別說明符:_Thread_local
,以該關鍵字聲明的對象具有線程存儲期,意思是線程中聲明的對象在該線程運行期間一直存在,且在線程開始時被初始化,因此這種對象屬於線程私有。
存儲類別 | 存儲期 | 作用域 | 鏈接 | 如何聲明 |
---|---|---|---|---|
自動 | 自動 | 塊 | 無 | 在塊中 |
寄存器 | 自動 | 塊 | 無 | 在塊中,用關鍵字register |
靜態、外部鏈接 | 靜態 | 文件 | 外部 | 在所有函數外部 |
靜態、內部鏈接 | 靜態 | 文件 | 內部 | 在所有函數外部,使用關鍵字static |
靜態、無鏈接 | 靜態 | 塊 | 無 | 在塊中,使用關鍵字static |
線程、外部鏈接 | 線程 | 文件 | 外部 | 在所有塊外部,使用關鍵字_Thread_local |
線程、內部鏈接 | 線程 | 文件 | 內部 | 在所有塊的外部,使用關鍵字static和_Thread_local |
線程、無鏈接 | 線程 | 塊 | 無 | 在快中,使用關鍵字static和_Thread_local |
自動變量
屬於自動存儲類別的變量具有自動存儲期
、塊作用域
且無鏈接
。默認情況下,聲明在塊和函數頭中的任何變量都屬於自動存儲類別,當然也可以顯示使用關鍵字auto
。
自動存儲期的變量意味着程序在進入該變量聲明所在的塊時變量存在,程序退出該塊時變量消失。分配的空間被回收,可做他用。
塊作用域和無鏈接意味着只有在該變量定義的塊中才能通過變量名訪問該變量(當然,參數用於傳遞變量的值和地址給另一個函數,這是間接的方法),並且內層塊會隱藏外層塊的定義。
關於自動存儲類變量,需要注意其不會初始化,除非顯示初始化,否則變量的初始值可能爲任意值。
注意:關鍵字auto
是存儲類別說明符(storage-class specifier)。auto
關鍵字在C++中的用法完全不同,如果編寫C/C++兼容的程序,最好不要使用auto
作爲存儲類別說明符。
寄存器變量
寄存器變量的屬性和自動存儲類幾乎一樣,都是塊作用域
、無鏈接
和自動存儲期
。使用存儲類別說明符register
來聲明寄存器變量。
int main (void)
{
register int quick;
register
類別聲明瞭一個請求,編譯器會根據寄存器或最快可用內存的數量衡量你的請求,從而加快訪問速度;亦可能或略你的請求,使得寄存器變量變成普通的自動變量。但仍不能對該變量使用地址運算符。
塊作用域的靜態變量
塊作用域的靜態變量具有靜態存儲期
、塊作用域
和無鏈接
,靜態變量中的靜態表示該變量在內存中保持不變,並不是值不變。之前提過,可以創建具有靜態存儲期
、塊作用域
的局部變量,這些變量和自動變量一樣,具有相同的作用域,但是程序離開其所在函數後,變量不會消失。即具有靜態存儲期
。在塊中(提供塊作用域
和無鏈接
)以存儲類別說明符static
聲明這種變量。
此處static
是第二種用法。對一個具有塊作用域
的變量用static
修飾,改變了變量的存儲期。
void trystat(void)
{
int fade = 1;
static int stay = 1;
printf("fade = %d and stay = %d\n", fade++, stay++);
}
上述代碼中,靜態變量stay
保存了它被遞增1
後的值,但是fade
變量每次都是1
。這表明初始化的不同,每次調用trystat()
都會初始化fade
,但是stay
只在編譯trystat()
時被初始化一次,之後每一次運行trystat
函數都不會爲stay
分配新的內存空間,因爲它一直存在着,所佔用的空間從沒被回收。
fade
聲明每次調用函數時都會執行,這是運行時行爲。而stay
聲明實際上並不是trystat
函數的一部分,因爲靜態變量和外部變量在程序被載入內存時已執行完畢,把聲明放在函數中只是告訴編譯器只有在trystat
函數中才能看到該變量,這條聲明未在運行時執行。
即靜態存儲期保證其在整個程序運行期間都存在,而塊作用域確定只有在函數塊內訪問該變量。
外部鏈接的靜態變量
外部鏈接的靜態變量具有文件作用域
、外部鏈接
和靜態存儲期
。
該類別有時被稱爲外部存儲類別(external storage class)
, 屬於該類別的變量成爲外部變量(external variable)
。把變量的定義性聲明放在所有函數的外面便創建了外部變量。爲了表明函數使用了外部變量,可以再函數中用關鍵字external
再次聲明。如果一個源文件使用的外部變量定義在另一個源文件中,則必須用extern
在該文件中聲明該變量。如下所示
int Errupt; // 外部存儲類變量
double Up[100]; // 外部存儲類變量
extern char Coal; // 必須聲明,說明Coal被定義在其他文件中
void next(void);
int main(void)
{
extern int Errupt; // 可選聲明
extern double Up[]; // 可選聲明
...
}
void next(void)
{
...
}
若在main
函數中,有以下代碼
int Errupt;
則該變量爲自動變量,是一個獨立的局部變量,與外部變量Errupt
不同。該局部變量Errupt
僅main
函數可見,而且在執行塊中的語句時,會隱藏外部同名變量Errput
.而對於該文件的其他函數來說,外部變量Errupt
仍可見。
需要注意的是,若外部變量沒有進行顯示初始化,會自動初始化爲0
,而且如果要對外部變量初始化,只能使用常量進行初始化。
int x = 10; // 沒問題,10爲常量
int y = 3 + 20; // 沒問題,用於初始化的爲常量表達式
size_t z = sizeof(int); // 沒問題,用於初始化的爲常量表達式
int x2 = 2 * x; // 不行,x爲變量
內部鏈接的靜態變量
該存儲類別的變量具有靜態存儲期
、文件作用域
和內部鏈接
。在所有函數內部(這點與外部變量相同),用存儲類別說明符static
定義的變量具有這種存儲類別。此處的static
改變了變量的鏈接屬性
static int svil = 1; // 靜態變量,內部鏈接
int main(void)
{
普通的外部變量可用於同一程序中任意文件中的函數,但是內部鏈接的靜態變量只能用於同一個文件中的函數。可以使用存儲類別說明符extern
,在函數中重複聲明任何具有文件作用域的變量,這樣的聲明並不會改變其鏈接屬性。
存儲類別說明符
C中共有6
個關鍵字作爲存儲類別說明符
- auto:表明變量是自動存儲期,只能用於塊作用域的變量聲明中。由於在塊中聲明的變量本身就具有自動存 儲期,故使用
auto
主要是爲了明確表達要使用與外部變量同名的局部變量的意圖 - register:也只用於塊作用域的變量,它把變量歸爲寄存器存儲類別,請求以最快速度訪問該變量。同事,還保護了該變量的地址不被獲取
- static:該說明符創建的對象具有靜態存儲期,載入程序時創建對象,當程序結束時對象消失。如果
static
用於文件作用域聲明,作用域受限於該文件;如果static
用於塊作用域聲明,作用域受限於該塊。因此,只要程序在運行對象就存在並保留其值,但只有在執行塊內的代碼時,才能通過標識符訪問。塊作用域的靜態變量無鏈接。文件作用域的靜態變量具有內部鏈接。 - extern:表明聲明的變量定義在別處。如果包含
extern
的聲明具有文件作用域,則引用的變量必須具有外部鏈接。如果包含extern
的聲明具有塊作用域,則引用的變量可能具有外部鏈接或內部鏈接,這取決於該變量的定義式聲明。 - _Thread_local
- typedef:
typedef
關鍵字與任何內存存儲無關,只是語法的原因被歸於此類。
注意:絕大多數情況下,不能在聲明中使用多個存儲類別說明符,所以這意味着不能使用多個存儲類別說明符作爲typedef
的一部分。唯一例外的是_Thread_local
,它可以和static
或extern
一起使用。
參考
- C Primer Plus
- C語言中的內存管理那些事0-基本概念
- C語言中的內存管理那些事1-存儲類
- C語言存儲類別(Storage Class)