C Primer Plus--C存儲類、鏈接和內存管理之存儲類(storage class)

存儲類


C爲變量提供了5種不同的存儲類型:

  • 自動
  • 寄存器
  • 具有代碼塊作用域的靜態
  • 具有外部鏈接的靜態
  • 具有內部鏈接的靜態

不同角度描述變量:

  • 存儲時期 變量在內存中保留的時間
  • 變量作用域(Scope)以及它的鏈接(Linkage) 變量的作用域和鏈接一起表明程序的哪些部分可以通過變量名來訪問該變量

不同的存儲類提供了變量的作用域、鏈接以及存儲時期的不同組合。

作用域

作用域分爲:

  • 代碼塊作用域(block scope)
    代碼塊在C中指的是一對花括號之間。定義在代碼塊之間的變量其可見性僅存在於其定義處和閉花括號之間。函數的形式參量、for、while、if-else等中定義的變量都是屬於代碼塊作用域。
  • 函數原型作用域(function prototype scope)
    函數原型作用域從變量定義處一直到函數原型結尾,這解釋了爲什麼我們平時定義函數原型的時候的形式參量名字可以與函數定義中形參名字不同,甚至根本沒有名字:編譯器只關心原型形參的數據類型,因爲函數原型形參變量作用域極短,其名稱並不重要。
    int function(int arg1, int arg2);
    int function1(int, int);
    
    但是有些情況下函數原型裏的形參名稱有作用:存在變長數組參量時。
    void function2(int n, int m, int array[m][n]);
    
    變長數組array中使用的變量m n是之前已經聲明的。
  • 文件作用域(file scope)
    一個在所有函數之外定義的變量具有文件作用域。具有文件作用域的變量其可見性存在於其定義處到文件結尾處。這樣的變量也被成爲全局變量(global variable)
    int function(int arg1, int arg2);
    int file_scope = 0;
    int main() {
        return 0;
    }
    int function(int a, int b){
    
    }
    
    這裏面file_scope對於mainfunction兩個函數都是可見的。

鏈接

C變量具有下列三種鏈接之一:

  • 外部鏈接 external linkage
  • 內部鏈接 internal linkage
  • 空鏈接 no linkage

具有函數原型作用域或者代碼塊作用域的變量具有空鏈接,他們是由其定義所在的代碼塊或者函數原型私有的。具有文件作用域的變量可能有內部或者外部鏈接。有外部鏈接的變量可以在一個多文件程序任何地方進行訪問。一個具有內部鏈接的變量可以在其所在的單個文件裏任何地方訪問。
區分一個具有文件作用域是外部鏈接還是內部鏈接可以看static關鍵字。

static int full_global = 0;//內部鏈接 只在本文件中全局可見
int file_global = 1;//外部鏈接 full_global可以被同一程序多個文件訪問

存儲時期

C變量可以有以下兩種存儲時期之一:

  • 靜態存儲時期 static storage duration
    具有靜態存儲時期的變量會在程序執行期間一直存在。具有文件作用域的變量是具有靜態存儲時期的。
  • 自動存儲時期 automatic storage duration
    具有代碼塊作用域的變量一般情況下具有自動存儲時期。這樣的變量在程序進入代碼塊時被分配內存,退出代碼快時其內存被釋放。

由以上作用域、鏈接、存儲時期得到了五種存儲類:

存儲類 存儲時期 作用域 鏈接 聲明方式
自動 自動 代碼塊 代碼塊內
寄存器 自動 代碼塊 代碼塊內,使用關鍵字register
具有外部鏈接的靜態 靜態 文件 外部 所有函數之外
具有內部鏈接的靜態 靜態 文件 內部 所有函數之外,使用修飾符static
空鏈接的靜態 靜態 代碼塊 代碼塊內,使用修飾符static

自動變量

默認情況下,在代碼塊或函數的頭部定義的任意變量都屬於自動存儲類型。也可以使用關鍵字auto來顯示的表明此變量爲自動變量:auto int auto_var = 0;,這樣做的目的可以是顯式覆蓋一個外部函數定義的同名變量或者強調該變量的存儲類型不可以改變爲其他存儲方式。auto稱爲存儲類說明符(storage class specifier)。
自動變量具有代碼塊作用域和空鏈接,這樣只有在變量定義的代碼塊裏纔可以通過變量名訪問該變量。C也允許通過向函數傳遞參數地址來訪問變量,但這樣屬於間接訪問,不是通過變量名直接訪問的。
覆蓋(hide)指的是不同作用域的變量名稱相同的情況下,會根據程序所在環境按名稱訪問相應的變量值。舉個栗子:

int x = 0;
while (x++ < 3){
     int x = 10;
     x++;
     printf("%d\n",x);
 }
printf("%d",x);

輸出:

11
11
11
4

while循環每次判斷用的是外層x的值,進入代碼塊後,x被重新定義並初始化,代碼塊裏面使用的是暫時的x,每次循環退出時x會被銷燬,因此也就有了上面的輸出。寫程序千萬不要寫這種代碼!!

自動變量不會被自動初始化,必須得顯式初始化!但是全局變量會存在默認初始化!

寄存器變量

變量一般存儲在計算機的內存中,可以通過關鍵字register來顯式得申請將變量存儲在CPU寄存器中或者存儲在速度最快的可用內存來達到更快的訪問和操作速度。但是,顯式得聲明變量爲寄存器變量並不會導致該變量一定是寄存器變量,這需要編譯器考慮到寄存器數目或者高速可用內存數量,如果不行,那麼該變量就會變爲自動變量。寄存器變量與自動變量有相同的代碼塊作用域、空鏈接、自動存儲時期,但是無法使用&操作符獲取寄存器變量的地址,即使沒有申請成功,該變量爲自動變量,也還是無法獲取它的地址。
register關鍵字能夠申請的類型是有限的,像C primer plus裏提到了處理器可能沒有足夠大的寄存器來容納double類型的數據。

具有代碼塊作用域的靜態變量

靜態變量並不是指變量不可變,而是指變量的位置固定不動。在代碼塊內部使用修飾符static聲明變量會產生具有代碼塊作用域、空鏈接但靜態的變量。
栗子:

#include<stdio.h>

void block_static(void);

int main(int argc,char *argv[]) {
    for (int i = 0; i < 4; ++i) {
        printf("loop %d\n:",i);
        block_static();
    }
    
    return 0;
}

void block_static(void){
    int test = 10;
    static int foo = 100;
    printf("test = %d, foo = %d\n",test++,foo++);
}

輸出結果:

loop 0
:test = 10, foo = 100
loop 1
:test = 10, foo = 101
loop 2
:test = 10, foo = 102
loop 3
:test = 10, foo = 103

靜態變量foo的內存位置固定不變,每次block_static函數執行時都會訪問它的值,它每次增加的值並沒有丟失,static int foo = 100;這個語句既在block_static第一次執行時聲明瞭靜態變量foo,之後在block_static每次執行時告訴這個函數foo對其是可見的,它知道這個變量的地址,它可以去訪問。而test變量就不同,它是自動變量,每次blocl_static執行時,test先被分配內存並初始化,執行結束時內存被回收,丟失這個值,下一次函數執行時已經丟失了對原先test內存地址訪問權,會重新創建test並分配內存然後銷燬,周而復始。
形式參量無能使用static修飾。

具有外部鏈接的靜態變量

具有外部鏈接的靜態變量具有文件作用域、外部鏈接和靜態存儲時期,也被稱爲外部變量。
當想要創建一個外部變量時,把變量定義在所有函數之外即可,也可以顯式地使用 extern關鍵字來聲明。當變量是在別的文件定義的時,必須使用extern聲明變量。

#include<stdio.h>

int Global = 100;


int main() {

    extern int Global;//這一行其實可以省略

    return 0;
}

上面的例子中main函數要訪問Global變量無須聲明,這樣做只是爲了表明main函數需要用它而已。需要注意的是如果這樣寫:extern int Global = 10;是會出錯的,因爲外部變量不允許在函數內部進行定義。變量的聲明與定義是不同的。這裏Global既然是外部變量,就不允許函數內部對其進行修改(具體見此處)。另外,如果把extern去掉會產生一個自動變量Global將外部變量Global覆蓋掉。
外部變量可以顯式地初始化,但是若是沒有顯式初始化,那麼默認會被賦值爲0。而且,只可以用常量表達式來對外部變量進行初始化

int Global = 100;
int y = 3 + 10;//合法,3+10是一個常量表達式
size_t z = sizeof(int);//合法,sizeof是編譯時運算符,當其操作數不爲變長數組時,返回值爲一個常量
int x = y; //不合法,y是一個變量

int main() {

    extern int Global;

    return 0;
}

extern關鍵字

int tern = 1;//外部變量tern定義,初始化

int main(void){
	extern int tern;
	return 0;
}

在上面的代碼段中tern有兩次聲明,第一次是對外部變量的定義聲明爲變量分配了內存空間,第二處因爲有extern存在,表明是對外部變量extern的引用,爲引用聲明,並不涉及內存分配。在C Primer Plus中這樣說:關鍵字extern表明該聲明不是一個定義,因爲它指示編譯器參考其他地方。
如果這樣做:

extern int tern;
int main(void){
}

那麼編譯器會假定tern是在其他文件中定義的外部變量,而不會引起空間分配。因此,不要用extern來進行外部定義,只用它來引用一個已經存在的外部定義
一個外部變量只允許一次初始化,必須在外部變量被定義聲明的同時進行初始化。
extern int tern = 1000;不合法,因爲此時編譯器假定tern已經存在,這是一個引用聲明,不是定義聲明。不能對tern進行二次初始化。

具有內部鏈接的靜態變量

具有內部鏈接的靜態變量具有靜態存儲時期、文件作用域以及內部鏈接。它通過static關鍵字在函數外部進行定義。
這種變量不同於外部變量,它只能被同文件中的函數進行訪問,在函數中同樣可以使用extern關鍵字來進行引用聲明,但不會改變它的內部鏈接。

int global_full = 100;//外部鏈接、文件作用域、靜態
static int global_not_full = 10;//內部鏈接、文件作用域、靜態

int main() {

    extern int global_full;
    extern int global_not_full;//global_not_full仍是內部鏈接
    //這兩種其實都多餘,main中可以直接訪問這兩個變量
    return 0;
}

多文件

當一個C程序包含多個獨立代碼文件時,若需要共享外部變量,這時候外部鏈接、內部鏈接才顯得重要。通過在一個文件中定義外部變量,在其他文件中引用聲明這個變量實現共享。除了這個本身的定義聲明之外,其他所有生命都必須使用關鍵字extern來進行引用,並且只能在定義的時候初始化一次。注意,其他文件要想使用這個變量,必須顯示的使用extern聲明,否則該變量不能用於其他文件。

這是我對《C Primer Plus》第十二章存儲類、鏈接和內存管理所寫的一部分筆記,未完待續。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章