C語言中儲存類別、連結與記憶體管理
第12章 儲存類別、連結和記憶體管理
通過記憶體管理系統指定變數的作用域和生命週期,實現對程式的控制。合理使用記憶體是程式設計的一個要點。
12.1 儲存類別
C提供了多種不同的模型和儲存類別,在記憶體中儲存資料。
被儲存的每一個值都佔用一定的實體記憶體;C語言把這樣一塊記憶體稱為物件(object)。
物件可以儲存一個或多個值。或者並未儲存實際的值。
C不是面向物件的語言,面向物件程式設計中的物件指的是類物件。
程式需要一種方法訪問物件 ---> 通過宣告變數來完成
int entity =3;
該宣告建立了一個名為entity的識別符號(identifier
識別符號可以用來指定特定物件的內容。識別符號遵循變數的命名規則。
識別符號是軟體指定硬體記憶體中的物件的方式。
變數名(識別符號)不是唯一指定物件的途徑。
int * pt = &entity;
*pt不是一個識別符號,它不是一個名稱。它確實指定了一個物件。
左值:指定物件的表示式;*pt 和entity都是左值。
再舉一個例子:const char * pc = “Behold a string literal!”
const限定符保證被pc指向的內容不被修改。但是不能保證pc不指向別的字串;
儲存期(storage duration
識別符號(identifier)用於訪問物件。
可用作用域(scope)和連結(linkage)描述識別符號。
識別符號的作用域和連結表明了程式的哪些部分可以使用它。
不同的儲存類別具有不同的儲存期、作用域和連結。
識別符號可以再原始碼的多檔案中共享,可用於特定檔案的任意函式中,可僅限於特定函式中使用。
物件可存在於程式的執行期,也可以僅存在於它所在函式的執行期。對於併發程式設計,物件可以再特定執行緒的執行期存在。可以通過函式呼叫的方式顯式分配和釋放記憶體。
12.1.1 作用域
塊:是用花括號
1、塊作用域:(block scope)
塊作用域中的變數可見範圍是從定義處到包含該定義的塊的末尾。注意:形式引數宣告在函式花括號之前,但是它們也具有塊作用域,也是屬於函式這個塊。C99把塊的概念擴充套件到了for迴圈、while迴圈、do while迴圈和if語句所控制的程式碼,即是這些程式碼沒有被花括號括起來,也算是塊的一部分。一旦離開塊,就不能再被訪問。
2、函式作用域:(function scope)
3、函式原型作用域:(function prototype scope)
函式原型作用域的範圍是從形參定義處到原型宣告結束。編譯器在處理該函式原型時不關心形參名,只關心它的型別。即使有形參名,也不必和函式定義中匹配。
只有在變長陣列中,形參名才有用:void use_a_VLA(int n,int m, ar[n][m]);
方括號中必須使用在函式原型中已宣告的名稱。
4、檔案作用域:(file scope)
變數定義在函式的外面,具有檔案作用域。具有檔案作用域的變數,從它的定義到該定義所在檔案的末尾均可見。檔案作用域變數也稱為全域性變數。
注意:翻譯單元和檔案
每個翻譯單元均對應一個原始碼檔案和它所包含的檔案。
原始碼(.c)中包含一個或多個頭檔案(.h副檔名)。
C預處理實際上是用包含的標頭檔案的內容替換#include指令。
編譯器原始碼檔案和所有的標頭檔案都看成是一個包含資訊的單獨檔案。這個檔案被稱為翻譯單元(translation unit)。描述一個具有檔案作用域的變數時,它的實際可見範圍是翻譯單元。
12.1.2 連結
C變數具有3種連結屬性:外部連結、內部連結或無連結。
具有塊作用域、函式作用域或函式原型作用域的變數都是無連結變數。意味著這些變數只屬於定義它們的塊、函式或原型私有。
具有檔案作用域的變數可以是外部連結或內部連結。
外部連結變數可以再多檔案程式中使用,內部連結變數智慧在一個翻譯單元中使用。
非正式術語:
“內部連結的檔案作用域”描述僅限於一個翻譯單元。->簡化稱呼,叫作“檔案作用域”;
“外部連結的檔案作用域”描述可延伸至其他翻譯單元的作用域。->簡化稱呼,叫作“程式作用域”。
如何知道檔案作用域變數是外部連結還是內部連結?可以檢視外部定義中是否使用了儲存類別說明符。
12.1.3 儲存期
作用域和連結描述了識別符號的可見性。
儲存期描述了通過這些識別符號訪問的物件的生存期。
C物件有4個儲存期:靜態儲存期、執行緒儲存期、自動儲存期、動態分配儲存期。
如果物件具有靜態儲存期,那麼它在程式的執行期間一直存在。
檔案作用域變數:預設具有靜態儲存期。
對於檔案作用域變數,關鍵字static表明了其內部連結屬性,而非儲存期。
以static宣告的檔案作用域變數具有內部連結。
所有的檔案作用域變數都具有靜態儲存期。
執行緒儲存期用於併發程式設計,程式執行可被分為多個執行緒。具有執行緒儲存期的物件,從被宣告時到執行緒結束一直存在。以關鍵字_Thread_local宣告一個物件時,每個執行緒都獲得該變數的私有備份。
塊作用域的變數通常都具有自動儲存期。當程式進入定義這些變數的塊時,為這些變數分配記憶體。當退出這些塊時,釋放剛才為變數分配的記憶體。這種做法相當於把自動變數佔用的記憶體視為一個可重複使用的工作區或暫存區。自動變數離開函式時會被銷燬。
變長陣列稍有不同,它們的儲存期從宣告處到塊的末尾。而不是塊的開始和塊的末尾。
塊作用域的變數也可以宣告為靜態變數。這樣可以方便其他函式通過間接的方式訪問該變數,例如通過指標形參或返回值。
12.1.4 自動變數
預設情況下,宣告在塊或函式頭中的任何變數都屬於自動儲存類別。
如果為了更清楚表達你的意圖,可以顯式使用auto關鍵字。
但是注意C++中auto關鍵字的用法完全不同。如果寫C/C++相容的程式,最好不要用auto作為儲存類別說明符。
塊作用域和無連結意味著只有在變數定義所在的塊中才能通過變數名訪問該變數(引數用於傳遞變數的值和地址給另一個函式,這是間接的方法)。另一個函式可以使用同名變數,但是該變數是儲存在不同記憶體位置上的另一個變數。
如果內層塊中宣告的變數與外層塊中的變數同名會怎樣?內層塊會隱藏外層塊的定義。但是離開內層塊後,外層塊變數的作用又回到了原來的作用域。
1、沒有花括號的塊
作為迴圈或if語句的一部分,即使不使用花括號({}),也是一個塊。更完整地說,整個個迴圈是它所在塊的子塊。迴圈體是整個迴圈塊的子塊。與此類似,if語句是一個塊,與之相關聯的子語句是if語句的子塊。
2、自動變數的初始化
自動變數不會初始化,除非顯式初始化它。
如果沒有初始化,別指望整個值是0;
可以用非常量表達式,初始化自動變數,前提是所用的變數已在前面定義過。
例如:
int main(void)
{
int ruth = 1;
int rance = 5* ruth;
}
12.1.5 暫存器變數
變數儲存在計算機記憶體中。
如果幸運的話,暫存器變數儲存在CPU的暫存器中。或者概括地說,儲存在最快的可用記憶體中。
與普通變數相比,訪問和處理這些變數的速度更快。由於暫存器變數儲存在暫存器而非記憶體中,所以無法得到暫存器變數的地址。絕大多數方面,暫存器變數和自動變數都一樣,也就是說它們都是塊作用域、無連結和自動儲存期。
使用儲存類別說明符:register便可宣告暫存器變數;
int main(void)
{
register int quick;
}
“如果幸運”是因為宣告變數register類別與直接命令相比更像是一種請求。請求成功就成了暫存器變數,不成功就變成普通的自動變數。編譯器會根據暫存器或最快可用記憶體的數量衡量這個請求。即便是這樣,仍然不能對該變數使用求址運算子。
可宣告為register的資料型別有限,例如處理器中的暫存器可能沒有足夠大的空間來儲存double型別的值。
12.1.6 塊作用域的靜態變數
靜態變數(static variable):意思是該變數在記憶體中原地不動,並不是說它的值不變。靜態可以是指它的生存期很長,跟程式一樣。
如果是建立靜態儲存期、塊作用域的區域性變數,這些變數與自動變數一樣,具有相同的作用域。但是程式離開它們所在的函式後,這些變數不會消失。
這種變數具有塊作用域、無連結,但是具有靜態儲存期。計算機在多次呼叫之間會記錄它們的值。
注意不能在函式的形參中使用static;
注意 static int stay =1; ---> 函式中有這麼一條語句的話,只在編譯時被初始化一次。下次再呼叫函式時,不會再執行這條語句。
塊作用域的靜態變數和檔案作用域的靜態變數不一樣;static關鍵字表達的含義不同;
12.1.7 外部連結的靜態變數
外部連結的靜態變數具有檔案作用域、外部連結和靜態儲存期。
該類別有時被稱為外部儲存類別(external storage class)。
屬於該類別的變數稱為外部變數(external variable)。
如果一個檔案使用的外部變數定義在另一個原始檔中。則必須用extern在該檔案中宣告該變數。
extern char Coal;
如果省略了extern關鍵字,相當於建立了自動變數:char Coal;
1、初始化外部變數
外部變數和自動變數類似,也可以被顯式初始化。
但是如果未初始化外部變數,它們會被自動初始化為0。
這點與自動變數不同,自動變數如果沒有初始化,其值是隨機的。
而且注意:只能使用常量表達式初始化外部變數,這點與自動變數不同。
2、使用外部變數
int main()
{
extern int units; //這麼做是為了指出該函式要使用這個外部變數。這是一個引用式宣告。
}
3、外部名稱
4、定義和宣告
定義式宣告和引用式宣告是不一樣的。關鍵字extern表明該宣告不是定義。因為它指示編譯器去別處查詢其定義。所以定義式宣告會引發記憶體分配儲存空間。而引用式宣告會指示編譯器去尋找定義。
12.1.8 內部連結的靜態變數
該儲存類別的變數具有:靜態儲存期、檔案作用域、內部連結;
在所有函式外部(這點與外部變數相同);
用於儲存類別說明符(static)定義的變數具有這種儲存類別。
例如:
static int svil = 1; //靜態變數,內部連結
int main(void)
{
…
}
12.1.9 多檔案
只有檔案由多個翻譯單元組成時,才能體現內部連結和外部連結的重要性。
複雜的C語言程式通常由多個單獨的原始碼檔案組成。有些時候,這些檔案需要共享一個外部變數。C語言通過在一個檔案進行定義式宣告。然後在其他檔案通過引用式宣告來實現共享。也就是說,除了一個定義式宣告外,其他宣告都要使用extern關鍵字。而且,只有定義式宣告才能初始化變數。
外部變數定義在一個檔案中,其他變數在使用它之前必須先宣告它。也就是說,在某檔案中對外部變數進行定義式宣告知識單方面允許其他檔案使用該變數,其他檔案在用extern關鍵字宣告之前不能直接使用它。
12.1.10 儲存類別說明符
關鍵字extern和static的含義取決於上下文;
C語言中有6個關鍵字:auto、register、static、extern、_Thread_local、typedef
auto說明符表明變數是自動儲存期,只能用於塊作用域的變數宣告中。由於塊中宣告的變數本身就預設具有自動儲存期。所以使用auto主要是為了明確表達要使用與外部變數同名的區域性變數的意圖。
register說明符也只用於塊作用域的變數。把變數歸為暫存器的儲存類別。請求最快速度訪問該變數。同時,還保護了該變數的地址不被獲取。
static說明符建立的物件具有靜態儲存期,載入程式時建立物件,當程式結束時物件消失。如果static用於檔案作用域宣告,作用域受限於該檔案。如果static用於塊作用域宣告,作用域則受限於該塊。
extern說明符表明宣告的變數定義在別處。如果包含extern的宣告具有檔案作用域,則引用的變數必須具有外部連結。如果包含extern的宣告具有塊作用域,則引用的變數可能具有外部連結或內部連結,這取決於該變數的定義式宣告。
12.1.11 儲存類別和函式
函式也有儲存類別;
可以是外部函式(預設)或靜態函式;
C99還新增了第3種類別,行內函數;
外部函式可以被其他檔案的函式訪問,靜態函式只能用於其定義所在的檔案。
static說明符所建立的函式,屬於特定模組私有。
用extern關鍵字宣告定義在其他檔案中的函式。
除非使用static關鍵字,否則一般函式宣告都預設為extern;
12.1.12 儲存類別的選擇
隨意使用外部儲存類別的變數導致的後果遠遠超過了它所帶來的便利。
const資料,它們在初始化之後就不會被修改,所以不用擔心它們被意外修改。
保護性程式設計的黃金法則是:“按需知道”原則。
儘量在函式內部解決該函式的任務,只共享那些需要共享的變數。
除自動儲存類別外,其他儲存類別也很有用。不過,在使用其他儲存類別之前要考慮一下是否有必要這樣做。