全域變數的宣告與定義
雖然我們的編譯器還沒有全域變數,所以這節不會出現對應全域變數的組合語言指令作為範例,但是全域變數在組語階段和函式幾乎是一樣的。因此和函式一樣,全域變數也有分宣告和定義。所以如果變數本體出現在複數C程式檔中的話,通常會報連結錯誤。
全域變數預設是放在不可執行的記憶體區域內,所以要跳到該位址的話程式會發生記憶體區段錯誤(segment fault)然後崩潰(crash),但除此之外,資料和程式本質上是沒有不同的。執行時可以把函式作為資料像全域變數那樣讀取,也可以把記憶體的屬性改成可執行,跳躍到資料的位址,把資料作為程式執行。
函式和全域變數兩方本質上都只是記憶體裡的資料,我們實際用一段程式來確認看看吧。底下的程式是把main
這個識別碼定義為全域變數。main
的內容為x86_64的機械碼:
把上述的C程式碼存成 foo.c 後編譯,使用objdump
確認看看其內容吧。objdump
預設是顯示為16進制,加上-D
參數就可以強制把檔案作為程式反組譯:
預設把資料放在不可執行區域的作法,可以在編譯時加上-Wl,--omagic
參數來變更。現在來使用這個參數生成可執行檔:(譯註:譯者在 Windows 10下使用 WSL 的 Debian 無法成功完成此操作,請使用安裝 Linux 發行版的電腦或是虛擬機器來操作。)
函式和變數都被組譯變成單純的標籤,屬於同一個命名空間(namespace),連結器在整合複數目標檔的時候就不會在意誰是資料誰是函式了。因此main
在C的標籤就算被定義為資料,也可以像main
是函數的時候一樣成功連結。
執行看看生成出來的檔案吧:
如上所述,正確傳回了42。main
這個全域變數的內容被作為程式執行了。
在C的文法中,全域變數加上extern
就會變成宣告。底下是int
型態全域變數foo的宣告:
要寫包含foo
的程式時,就要把這行寫在標頭檔。然後,在某個C程式檔中要定義foo
,底下為foo
的定義:
此外,C語言中在初始化的時候全域變數會被初始化為0
,和用0
或{0, 0, ...}
、"\0\0\0\0..."
來進行初始化是一樣的意思。
像int foo = 3
這樣寫初始值的時候,請只在定義寫初始值。宣告只是為了告訴編譯器型態,不需要寫具體的初始值。編譯器看到全域變數的宣告也不會輸出指令,不需要知道其內部究竟是被初始化成什麼樣。
省略初始值的情況下,全域變數的定義和宣告就只差在extern
而已,雖然外表很像,但是宣告和定義是不同的東西。這部份概念請好好在此節區分清楚。
Linkers and Loaders, ISBN 978-1558604964, John R. Levine (1999) 1.2章
Perhaps surprisingly, these two basic linker functions — relocation and library search — appear to predate even assemblers, as Mauchly expected both the program and subprograms to be written in machine language.
Last updated