---
# System prepended metadata

title: 儲存類、作用域、生命週期、鏈結
tags: [C]

---

---
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`