作用域、連結屬性和儲存型別

扶磐發表於2021-02-13

最近在讀《程式設計師的自我修養——連結、裝載與庫》,感覺自己當初學習C的時候,對extern、static等關鍵字瞭解不是特別清晰,因此重溫了一遍《C和指標》中關於作用域、連結屬性和儲存型別的相關部分,加上了自己的理解,用部落格記錄一下。


作用域

當變數在程式的某個部分被宣告時,它只有在程式的一定區域才能被訪問

這個區域由識別符號的作用域決定,識別符號的作用域就是程式中該識別符號可以被使用的區域。

據我所學,編譯原理中有講到,檢查變數的作用域是否合乎規則,是在編譯中的語義分析時檢視的。

編譯器可以確認4種不同型別的作用域——檔案作用域、函式作用域、程式碼塊作用域和原型作用域。識別符號宣告的位置決定了它的作用域。

  • 程式碼塊作用域

    位於一堆花括號之間的所有語句稱為一個程式碼塊,任何在程式碼塊的開始位置宣告的識別符號具有程式碼塊作用域。表明他們可以被這個程式碼塊中的所有語句訪問。

    下圖中的a、b、c、d和arg均具有程式碼塊作用域。

    /* main.c */
    #include <stdio.h>
    int g;
    
    int func(int x);
    
    int main(int argc, char* argv[]) {
        int a;	
        int b;
        a = 5;
        {
            int c;
            int a;	//隱藏外部的a,外層的那個識別符號將無法在內層程式碼塊中通過名字訪問。
            c = 5;
            a = 10;
            printf("%d", c + a);	//列印結果:15,而不是10
        }
        {
            int d;
            func(d);
        }
    }
    
    int func(int arg) {
        //
    }
    

    注:我們應當避免在巢狀的程式碼塊中出現相同的變數名,因為並沒有很好的理由使用這種技巧,他們只會在程式的除錯或維護期間引起混淆。

  • 檔案作用域

    任何在所有程式碼塊之外宣告的識別符號都具有檔案作用域(file scope),他表示這些識別符號從他們的宣告之處直到他所在的原始檔結尾處都是可以訪問的。g、func和main都具有檔案作用域。這也就是為什麼我們要將func的宣告單獨寫在main函式前,就是為了main可以呼叫func函式,否則main是不可以訪問到func函式的。

  • 原型作用域

    原型作用域只適用於在函式原型中宣告的引數名,如func宣告語句中的x。

  • 函式作用域

    只適用於語句標籤,語句標籤用於goto語句。

後兩種作用域非常非常不常見,因此我們應當把關注點放在前兩個作用域上面。

連結屬性

識別符號的連結屬性(Linkage)決定如何處理在不同檔案中出現的識別符號。識別符號的作用域與它的連結屬性有關。但這兩個屬性並不相同。

/* main.c */
#include <stdio.h>
int g;

int func(int x);

int main(int argc, char* argv[]) {
    int a;	
    int b;
    a = 5;
    {
        int c;
        int a;	//隱藏外部的a,外層的那個識別符號將無法在內層程式碼塊中通過名字訪問。
        c = 5;
        a = 10;
        printf("%d", c + a);	//列印結果:15,而不是10
    }
    {
        int d;
        func(d);
    }
}

int func(int arg) {
    //
}
  • external

    屬於external連結屬性的識別符號不管宣告多少次,位於幾個原始檔都表示同一個實體。

    預設情況下,宣告在任何程式碼塊之外的變數或函式(即具有檔案作用域)具有external連結屬性,其餘都為none。程式碼中g、func和main連結屬性都是external,其餘的變數連結屬性均為none。

    extern關鍵字:

    • extern關鍵字為一個識別符號指定external連結屬性。
    • 對於檔案作用域即已經是extern連結屬性的變數,extern關鍵字是可選的
    • extern關鍵字用於原始檔中一個識別符號的第一次宣告時,它指定該識別符號具有extern連結屬性,但是如果該識別符號用於該識別符號的第2次或以後的宣告,他並不會更改由第一次宣告所指定的連結屬性。
  • internal

    具有internal連結屬性的識別符號在同一個原始檔內的所有宣告都指同一個個體,但位於不同原始檔的多個宣告則分屬不同的實體。

    如果某個宣告在正常情況下具有external連結屬性,在他面前加上static關鍵字,可以使他的連結屬性變為internal。例如如果g的宣告為static int g;,那麼變數g就變為原始檔私有。其他原始檔如果要連結g的變數,引用的是另一個不同的變數,類似的,函式宣告也可以是static,如static int func(int x);

    static只有對預設連結屬性為external的宣告才有改變連結屬性的效果。

  • none

    沒有連結屬性的識別符號(none)總是被當作單獨的個體,也就是說該識別符號的多個宣告被當作獨立不同的個體。

儲存型別

變數的儲存型別(storage class)是指儲存變數值的記憶體型別。變數的儲存型別決定變數何時建立、何時銷燬以及它的值將保持多久。有三個地方可以用於儲存變數:

  • 普通記憶體

    凡是在任何程式碼塊之外宣告的變數(具有檔案作用域、external連結屬性)總是儲存於靜態記憶體,這類變數稱為靜態變數,放在二進位制檔案的.data段或bss段中。

    靜態變數在程式執行之前建立,在程式的整個執行期間始終存在。他始終保持原先的值,除非給他附一個不同的值或程式結束。

  • 執行時堆疊

    在程式碼內部宣告的變數的預設儲存型別是自動的。也就是說他儲存於堆疊中,稱為自動變數。

    如果給他加上關鍵字static,可以使他的儲存型別從自動變為靜態(放在.data段或.bss段中)。具有靜態儲存型別的變數在整個程式執行過程中一直存在,而不僅僅在宣告它的程式碼塊的執行時存在。注意,修改變數的儲存型別並不表示修改該變數的作用域,他雖然始終存在,但是還是隻能在該程式碼塊內宣告過後,按名字訪問。

  • 硬體暫存器

    用於自動變數的宣告,提醒他們應該儲存於機器的硬體暫存器,而不是記憶體中,這類變數稱為暫存器變數。但是編譯器並不一定要理睬register關鍵字,也就是說不是你在變數前加了register關鍵字,這個變數最後就被儲存於機器的硬體暫存器裡面了,還是要看編譯器的”心情“的,即取決於編譯器的優化方案?。

    注:register變數是不提供地址的哦。

淺談初始化

初始化靜態變數不需要額外的時間和開銷,變數將會得到正確的值,如果不顯示地指定其初始值,靜態變數將初始化為0。因為靜態變數直接存在.data段或.bss段裡面,在生成目標檔案時已經被編譯器寫進去了,所以執行時肯定不花時間。

自動變數的初始化需要更多開銷因為當程式連結時還無法判斷自動變數的儲存位置。事實上,函式的區域性變數在函式的每次呼叫中可能佔據不同的位置,因此基於這個理由,自動變數沒有預設的初始值,而顯式的初始化將在程式碼塊的起始處插入一條隱式的賦值語句。這裡的隱式我認為就是程式碼段中插入了一條賦值語句如mov [ebp -4] , value,這樣的話就造成初始化和先宣告後賦值效率並無提高,只有風格之差。

Static和Extern

  • 當用於不同的上下文環境時,static關鍵字具有不同的意思。

    • 用於具有檔案作用域的變數或函式時,Static關鍵字可以改變他們的連結屬性,從external改為internal,但識別符號的作用域和儲存型別不受影響。函式照樣放在.text段中,全域性變數根據是否初始化放在.data段或.bss段中。
    • 用於具有程式碼塊作用域的變數時,其連結屬性為none,static不改變其連結屬性,而是修改變數的儲存型別,從自動變數改為靜態變數,作用域也不受影響。
  • extern關鍵字

    • 用於具有檔案作用域的變數或函式時,extern關鍵字是可選的,因為本身他們就具有external連結屬性,然而,如果你在其中一個地方定義變數,並在使用這個變數的其他原始檔的宣告中新增extern關鍵字,可以使讀者更好地瞭解你的意圖。
    • 用於具有程式碼段作用域的區域性變數時,extern關鍵字可以修改變數的連結屬性從none到external,這對我們在深度巢狀程式碼塊中引用全域性變數提供了一個途徑。

相關文章