作用域、鏈接屬性和存儲類型

最近在讀《程式設計師的自我修養——鏈接、裝載與庫》,感覺自己當初學習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,這對我們在深度嵌套程式碼塊中引用全局變數提供了一個途徑。
Tags: