C 語言的變數有所謂的 storage class, 初學時對當中的差異並不是很容易弄清楚, 後來我把各種條件稍作整理, 於是有了下面的表格:
條件/狀況 | 外部變數 (全域變數) |
靜態變數 | 自動變數 (區域變數) |
函數參數 | ||
---|---|---|---|---|---|---|
大分類 | 全域變數 | 區域變數 | ||||
Key Word | 無 | static | static | auto (可略) | 無 | |
定義/宣告註1位置 | 函數外部 | 函數內 (或者 {} 區塊內) | 函數本體 | |||
變數名稱重複 | 編譯錯誤. 專案中所有全域變數名稱需唯一 |
| ||||
在外部檔案 (.c/.h) 中使用(或參考) | 外部檔案中變數宣告的前面加上 extern 即可 | 外部檔案中無法使用(或參考) | 在定義的區塊外即無法使用 | 函數區塊外即無法使用 | ||
可視範圍註2 scope |
自定義位置 (不必需在檔案的最前面) 至檔案結束 | 函數內 (或者 {} 區塊內) | 函數內 | |||
生命期註3 life-time |
程式執行期間 (或永久) | 函數內 (或者 {} 區塊內) | 函數內 | |||
初值(未設初值時) | 0 (因為是使用 .bss 區段的關係) | 未知 | 編譯錯誤. 參數個數不一致註4 | |||
初值設定限制註5 及其執行時機 |
限制只能使用常數運算式 只執行一次, main() 執行前由 "可載入執行檔 ELF" (或二進位影像 binary image) 拷貝至正確之 .data 區段位址內 |
無限制, 任何運算式均可. 每一次進入函數或區塊後立即執行 |
無限制 呼叫前執行設定 | |||
預設使用之記憶體區段 | 無初值 | .bss註6 | stack註6 | register 或 stack註6 | ||
有初值 | .data註6 | stack註6 | register 或 stack註6 |
說明:
- 一共有 4 個 Storage Classes:
- 自動變數: automatic
- 外部變數: external
- 靜態變數: static
- 暫存器變數: register
- C 的變數大致分為二類: 外部變數 (external, 也可以稱為全域變數) 及自動變數 (automatic, 或者是區域變數). 二者區別是以變數定義出現在 .c 檔中的位置來區別: 由於 C 語言限制了函數區塊中不可以再定義函數, (C++ 也不支援, 這點和 Javascript 是不同的), 所以函數區塊內的就是自動變數, 函數區塊外的就是外部變數. 這二種變數定義時都不需加 keyword 修飾 (實際上, 外部變數沒有修飾字; 自動變數的修飾字是 auto, 但一般都省略)
- 外部變數不一定要出現在 .c 檔中的最前面, 可以安插在二個函數區塊的中間, 受影響的是該變數的 scope: 變數出現之前的函數無法使用該變數
- 靜態變數是外部變數或者自動變數在定義時多加上 keyword static, 主要改變二件事: 改變變數使用的記憶體區段, 及縮小可視範圍 (scope) 附帶一提: static 也可以加在函數定義的前面, 也是表示這個函數是內部函數, 可視範圍縮小到只在檔案內, 而且無法被外部檔案設 extern 來參考.
- Keyword register 則是要求改用 CPU 內部的暫存器來存放變數值, 而大部份 CPU 內部的暫存器都不多而且數量也不同, 所以只能加在自動變數前面. 而且不保證達成, 無法達成時就降級變成自動變數.
- 函數的參數則是一種特化的自動變數, 一般是預設使用 register 變數, 但是會依據 CPU 種類不同而有些變化. 例如:
- ARM CPU 的官方規範是函數的前 4 個參數會使用 r0~r3 來實作, 超過 4 個參數的部份則使用 stack.
- 但 intel x86 CPU 則
只有使用 register A. 其他無法使用使用 register 變數的參數都要使用 stack註6 (參考 Wiki X86 calling conventions) - Keil C51 則預設不用 stack, 改用自動變數, 詳細請參考這一篇C 語言:Keil C51 和標準 C 語言的差異
- 另外 C 還有二個變數的限制詞 (qualifier), const 和 volatile
- const 是限定該變數不能被修改. C 編譯器對於 const 變數的處理可以分為二類:
- 全域變數及靜態變數 (上表的前三欄)
- 區域變數 (上表的第四欄)
區段抄到 stack?? 沒錯, 從程式抄到 stack, 但它會融合在底層的機械碼 (machine code) 裡, 除非是陣列/字串才會在程式區段裡佔據一小段位置). 而對於加了 const keyword 的全域變數及靜態變數目前大部份 C 編譯器 (例如:gcc, ARM/MDK) 會使用 .text註6 區段, 這個區段可以在 link 時指定到 ROM/Flash 記憶體上. 但是 Keil C51 不會, Keil C51 要用 const code 才會是用 ROM, 只用 const 還是會佔用 DATA RAM) - volatile 這個字的中文翻譯是揮發物, 在這裡意思是指變數的內容值具有揮發性. 是限定不可以被快取到 CPU 內部的暫存器, 必需每次存取都真實的讀/寫該變數所佔用的位址 (意即:指定該變數的存取動作不作最佳化). 它經常出現在定義週邊晶片的暫存器, 尤其是週邊晶片的輸入暫存器部份一定要加 volatile 否則編譯出來的執行碼無法正確反應週邊晶片的現況. 還有多工環境中作為 semaphore 的變數也會要加上 volatile 這個 keyword.
- 高階 CPU 的使用者需要特別注意: volatile 這個 keyword 並不會 (也無沒) 控制快取記憶體 (cache memory) 的同步. 也就是說如果快取控制器設定錯誤 (或者可能是根本就無法控制快取範圍) 那這個 keyword 加或不加結果是一樣的 (被快取遮蔽了). 通常你需要特別劃定一個區域禁止快取; 或者每次存取這個區域的變數之前進行一次快取同步.
- const 和 volatile 可以同時出現 (不同於 storage class keyword 必需唯一), 即該變數是唯讀且揮發性 (常見於宣告硬體的狀態暫存器).
- const 是限定該變數不能被修改. C 編譯器對於 const 變數的處理可以分為二類:
備註:
- 定義會保留記憶體, 宣告則不會. 對變數來說除了 extern 是宣告之外, 其他的都是定義.
- 可視範圍: 是指在程式的那一個段落可以使用該變數. 自動變數 (使用 stack 堆疊區) 在離開定義他的 {} 區塊後就有可能被別人佔用, 所以可視範圍同生命期. 外部變數一離開定義的檔案就不可視, 但可以使用 extern 宣告來重新取得. (實際上, 幫我們完成這件事的是 linker). 但靜態變數則限制不可以被 extern 的宣告取得可視性. (compiler 的語法檢查阻止了我們取得其可視性)
- 生命期: 是指變數佔用記憶體的時間, 只有自動變數 (使用 stack 堆疊區) 可以重覆使用記憶體空間, 其他都是永久佔據 (embebbed system) 或載入執行時佔用.
- 指未使用 va_arg (變動個數參數)的情況. 若使用 va_arg, 需運用一些技巧檢查參數個數, 否則會發生使用錯誤的參數, 或者引發程式當掉.
- 初值設定指的在定義變數時, 跟隨在其後的等號 (=) 及其後的運算式
- .text, .bss, .data 是一般編譯器之預設記憶體區段名稱, linker (或者是 loader) 會安排實際的記憶體位址給各個區段.
- .text 為唯讀區段, 放置程式 (故也有其他 compiler 命名為程式區段 .code) 及常數資料 (小型 embedded system 會將之安置於 ROM/Flash 中)
- .data 為可讀寫區段, 放置初值不為 0 的變數 (使用 ROM/Flash 時會先附在 .text 之後). 在 main() 開始執行前, 由 "可載入執行檔 ELF" (或二進位影像 binary image) 拷貝至正確之 .data 區段位址內, 是故外部變數或靜態變數的初始值只能用常數運算式
- .bss 為可讀寫區段 (為 Block Starting Symbol 的縮寫, 維基百科), 放置初值為 0 的變數. 在 main() 開始執行前, .bss 區段會被清為 0
- stack 為堆疊區 (大陸用語: 棧) 一般是呼叫函數時的作業區 (返回位址暫存, 傳遞參數, 區域變數和返回值之儲存區).
- heap 為堆積區 (大陸用語: 堆) 是呼叫 malloc() 時取得記憶區塊的來源.
嵌入式系統 (有/無 作業系統) 二種執行模式下 (in Flash, in RAM) 的各個記憶體區段關係圖
進階作業系統 各種記憶體區段載入
及重新對應虛擬地址的關係
文章標籤
全站熱搜
留言列表