--- title: '儲存類、作用域、生命週期、鏈結' disqus: kyleAlien --- 儲存類、作用域、生命週期、鏈結 === ## OverView of Content :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) ::: [TOC] ## 儲存類 - 概述 儲存類:變量在 RAM 中會開闢一塊空間來做存取,並且 **記憶體被分為 `Heap 堆`、`Stack 棧`、`.data`、`.bass`、`.text`** ... 等等 ```c= int v1 = 10; // 以初始化,存在 .data 段 // 以下兩個初始化為 0,或是尚未初始化,放在 .bss 段 int v2 = 0; int v3; int main() { int v4 = 5; // Stack int* v5 = (int *)malloc(sizeof(int)); // Heap free(v5); return 0; } ``` :::success * 可以參考 [**程式 & 記憶體**](https://hackmd.io/X27YAUmYReydHpUrcsdpbw?view) 篇章 ::: ### Linux 記憶體概念 * 基礎段分類概念這裡不說明 (請參考 [**程式 & 記憶體**](https://hackmd.io/X27YAUmYReydHpUrcsdpbw?view)),特別說 Linux 中 C 應用程式的記憶體概念,這邊以常見的「外部裝置(也就是文件)」、「內核」映射為例 1. **文件映射區** * 文件是外部硬體提供,而 Linux 會將文件先讀取到核心 (Kernel) 中,在透過記憶體映射的方式,映射到目標應域的 RAM 中 (虛擬記憶體上) > 如此設計的原因是因為加快訪問速度,因為外部裝置的速度是最慢的 > > ![](https://i.imgur.com/S00q1f1.png) * 應用程序在操縱文件時,其實就是在操縱 Kernel 映射到當前 Process 的記憶體區塊 :::success 可參考 [**檔案映射 - mmap 動態拓展**](https://hackmd.io/ptxTEwCzQBuxv61dGL_lvA?view#%E6%AA%94%E6%A1%88%E6%98%A0%E5%B0%84---mmap-%E5%8B%95%E6%85%8B%E6%8B%93%E5%B1%95) 文章,該文章有做小實驗 ::: 2. **內核映射區**:將內核 (`Kernel`) 映射到應用記憶體中 * 每個應用進程都存活在獨立的進程空間中,每個進程有 0 ~ 3G 的使用者記憶體空間、1G 的內核空間 (這記憶體空間可能並非連續,這使用到了 **虛擬記憶體技術**) > 這裡的虛擬內存量,是以 32 位元系統為例 > > ![](https://i.imgur.com/Y79vBOy.png) ### auto - 修飾局部變數 * `auto` 關鍵字在 C 語言中,唯一的功能就是 **修飾局部變數**;表示該變量是自動局部變量 (依樣分配在 Stack 上) > 既然分配在 Stack 上,代表部初始化,其 Value 就是隨機的 平時在定義局部變量時只是省略個 `auto` 關鍵字,並且 **`auto` 不可以使用在全局變量** > ![](https://i.imgur.com/RB4j34a.png) * `auto` 也會自動推導,目前使用的區域變數類型 (沒定義類型的話會警告,但仍可編譯成功) ```c= void auto_test() { auto int apple = 10; auto book = 200; // 自動推斷 auto car = "car"; // 自動推斷 printf("Apple: %d\n", apple); printf("Book: %d\n", book); printf("Car: %s\n", car); } ``` > ![](https://i.imgur.com/FxCFLOf.png) ### static - 靜態變量 * `static` 關鍵字在 C 語言中有兩種用法,但這兩種用法沒相關,個代表了不同的意思 1. **修飾 ++局部變數++**:**透過 `static` 關鍵字修飾,該變量的儲存位置就不再是 `Stack`,也就是說該變量的數值會一直存在 !** * 已定義初始化:存在 `.data` * 未定義初始化:存在 `.bss` ```c= void static_local_test(int init) { static int a = 100; // .data static int b; // .bss int c; // stack if(init != 0) { b = 0; c = 0; } int bookcase[1000] = {0}; // 擾亂 Stack printf("last time b value: %d\n", b++); printf("last time c value: %d\n\n", c++); } int main(void) { static_local_test(1); static_local_test(0); static_local_test(0); return 0; } ``` > ![](https://i.imgur.com/HxdtVgH.png) 2. **修飾 ++全域變數++**:這有關於多檔案的鏈結,使用 `static` 描述的變數、函數,都不可以被其他檔案使用,只能在內部使用 > 鏈結時再說明 ### register - 提高變數讀寫 * `register` 簡單來說就是要求編譯器將變數放置到 CPU 等級的 Register 做存取(當然,這並不一定全部被的變數都會被放置到 Register 中) > 一般沒有聲明的變量是規劃在 RAM 記憶體空間 `register` 使用起來跟一般變量一樣 ```c= void register_test() { register int a = 100; // 規劃到棧存器 int b = 200; // 規劃到 RAM printf("a: %d\n", a); printf("b: %d\n\n", b); } ``` > ![](https://i.imgur.com/KqgCd0C.png) :::warning * **並非一定安排的到暫存器,畢竟 CPU 棧存器數量有限制,所以需慎用** ::: ### extern - 跨文件訪問 * 我們知道 C 語言是以文件為單位來做編譯,**如果要跨文件使用變量、函數,就需要使用 `extern` 關鍵字來修飾 (只能修飾全局變量)** * 這裡要注意一件事… **`定義`、`宣告` 兩者個差異**: * **宣告**:僅是告訴編譯器,有一個 **符號** * **定義**:同時有宣告的意義,並加上為該符號 **在記憶體中佔(耗費)一個位置** ```c= // storage.c int var = 10; // 宣告 + 定義 // ------------------------------------------------------ // extern_test.c extern int var; // 宣告 void test_extern() { var += 10; printf("extern val: %d\n", var); } ``` :::warning * 不需要再使用 `#include "storage.c"`,因為 extern 會去全局 (整個應用) 中尋找相對應的符號 ::: > ![](https://i.imgur.com/QDRTudc.png) ### volatile - 易變 * **使用 `volatile` 關鍵字有以下特點** * **變量由外部修改**:最常見的就是在中斷時改變變量數值,其次還有,在 mutli thread 時修改數值,硬體修改數值... 等等 * **寫入相同數值時**:不會再對記憶體寫入,使用 `volatile` 修飾變量後,就算是同樣的數值也會寫入 ```c= int a = 10; a = 10; // 由於是相同數值,所以會「被編譯器優化」 a = 10; a = 10; a = 10; a = 10; ``` * `volatile` 就是告訴編譯器不要隨意進行優化,使用方式如下 ```c= void test_volatile() { volatile int a, b, c; a = 3; b = a; c = b; // 如果沒修飾,可能被編譯器寫成 c = b = a = 3; printf("a: %d\n", a); printf("b: %d\n", b); printf("c: %d\n", c); } ``` ### restrict - 限制 :::warning **在 C99 之後才出現** > gcc 編譯可以使用 `-std=c99` 來指定編譯版本 ::: * `restrict` 關鍵字是 **用於限制、約束 ++指標++**,目的是為了讓編譯器更好的優化 簡單來說,**在範圍內,修飾的指標不會被參考** ```c= void test_restrict() { int a = 10; int* restrict aPtr; int* restrict bPtr; aPtr = &a; bPtr = &a; printf("aPtr: %d\n", *aPtr); printf("bPtr: %d\n", *bPtr); // 再次參考被 `restrict` 修飾的指標就會被警告 (仍編得過 int **aPtr2 = &aPtr; int **bPtr2 = &bPtr; printf("aPtr2: %d\n", **aPtr2); printf("bPtr2: %d\n", **bPtr2); } ``` > ![](https://i.imgur.com/6Tk50zP.png) ### typedef - 定義新類型 * typedef 在 C 中是屬於 **儲存類**,所以 **不可用第二個儲存類型關鍵字修飾** :::success * 使用請參考另一篇 [**指標 & Array & typedef**](https://hackmd.io/bzOkHGC_TDGN-RF4lK18Pw) ::: ## 作用域 - 概述 作用域的重點在 `{}` 符號 (`if`、`while`、`for`... 都有),只要變量一進入該符號就開始生命,離開就結束生命週期 ### 作用域 - 區域變量 * 相同符號是可以內蓋外 ```c= #include <stdio.h> int main(void) { int apple = 10; { int apple = 1; // 內部 symbol 會覆蓋外部 symbol apple += 1000; printf("Inner Apple price: %d\n", apple); } printf("Outer Apple price: %d\n", apple); return 0; } ``` > ![](https://i.imgur.com/XdyRhqo.png) ## 生命週期 - 概述 生命週期 * 開始:運行中分配變量空間,排斥其他變量操控 * 結束:回收變量空間,其他變量可用 ### Stack、Heap 1. Stack 跟 **作用域** 相當有關,生在作用域內,一離開作用域就死亡 ```c= void my_stack() { int a = 10; // a 被規劃 for(int b = 0; b < = 20; b++) { // b 被規劃 a += b; } // b 結束生命週期 printf("a: %d\n", a); } // a 結束生命週期 ``` 2. Heap 生命週期是使用者手動管理,生命週期從 `malloc` ~ `free` 之間,可以跨 Function,因為它的記憶體分配在 Heap 上,而非 Stack 上 > 操作不慎可能導致 OOM or 記憶體洩漏 ```c= int* alloc_int(int init) { // 生命週期開始 int *ptr = (int*) malloc(sizeof(int)); *ptr = init; return ptr; } int main(void) { int *a = NULL; a = alloc_int(9876); printf("a: %d\n", *a); free(a); // 生命週期結束 return 0; } ``` ### .data & .bss - 全局變量 * `.data` & `.bss` 用來描述全局變量,並且它們的 **生命周期是永久** (與應用同進退) * 至於其他段 `.text` & `.rodata` 也是生命周期永久 ### 函數 & Stack * 函數有幾個特點 1. 入參建議不要超過 4 個,超過建議使用 struct 包裹起來(或是使用指標) > 否則可能會造成 stack 的負擔 2. 傳入參數大小也不建議過大,避免超過 Stack Size,較大參數建議使用 Pointer 傳遞 3. 編譯完後函數 **會存在 elf 中的 `.text` 段** :::info * 可以使用 `readelf` 指令查看 > `readelf -S Hello.out` > ![](https://i.imgur.com/EDkKEiJ.png) ::: ### 函數 - 入棧 Stack * 函數的入棧是按照順序的,如下面程式的入棧順序就是 :::info * Stack 棧是由核心棧存器 `SP` 來管控,相關的核心棧存器還有 鏈結 `LR`、計數器 `PC` ::: 1. main 函數返回地址 2. main 函數 3. 遇到 subtraction 函數,保存 subtraction 函數完成後的返回地址 4. subtraction 函數入棧,**這裡又細分為,參數由右到左入棧** ```c= int subtraction(int a, int b) { return a - b; } int main(void) { int result = subtraction(3, 5); // Error 呼叫不到 subtraction 函數 } ``` Stack 概念圖如下 > ![](https://i.imgur.com/YIBLBdb.png) ## 鏈結 - 概述 鏈結:鏈結在 C 語言中主要就是在鏈結個個檔案的匯編結果 (`.o`),也就是將各個獨立的二進為檔鏈結,形成一個可執行檔; :::success * **編譯以文件為單位,鏈結以工程應用為單位** > ![](https://i.imgur.com/RH7mjfC.png) ::: ### 鏈結屬性 * C 語言中的鏈結有三種屬性 1. **外鏈結**:**使用 `extern` 修飾的全局變量**、**`函數` 都屬於外鏈結部分** ```c= extern int a; void printA() { printf("A: %d\n", a); } ``` :::info * 這在大型專案中容易會有重名的問題 ::: 2. **內鏈結**:使用 `static` 修飾的函數、**全局 static 變量** (不包含局部 static) ```c= static int count = 10; static void _cal_val(int *const val) { *val = *val + 100; } ``` :::success * `static` 可以解決部分函數、變量重名的問題;`static` 修飾過後即便其他檔案有,也不會相互衝突 ::: 3. **無鏈結**:**局部變量、`auto` 修飾、局部靜態變量** ```c= void my_local() { int a = 10; auto b = "Hello"; static c = 200; } ``` ## 更多的 C 語言相關文章 關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言 ### C 語言基礎 * **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」 :::info * [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) * [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/) * [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/) * [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) * [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: ### 編譯器、系統開念 * **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節 :::warning * [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) * [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/) * [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: ### C 語言與系統開發 * **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用 :::danger * [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) * [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/) ::: ## Appendix & FAQ :::info ::: ###### tags: `C`