C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性

C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性

Overview of Content

本文深入探討了C語言中各種儲存類別(如 autostaticregister 等)以及作用域(包括 局部變數全域變數等)的概念與作用。

我們將簡單介紹Linux系統中的記憶體概念,以及這些儲存類別如何影響變數的儲存位置和生命週期。此外,我們也將討論作用域的概念,並探討在C語言中函數與變數的作用域規則。

最後,我們將研究連結屬性,包括連結屬性的種類以及它們對變數和函數的影響(包括內、外連結)。

透過本文,讀者將全面了解C語言中各種儲存類別和作用域的運作方式,有助於提高對C語言程式的理解和最佳化能力。


C 語言 - 儲存類、修飾語

儲存類:變量在 RAM 中會開闢一塊空間來做存取,並且 記憶體被分為 Heap 堆Stack 棧.data.bass.text ... 等等


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;
}

● 可以參考 程式 & 記憶體 篇章


Linux 記憶體概念

● 基礎段分類概念這裡不說明 (請參考 程式 & 記憶體),特別說 Linux 中 C 應用程式的記憶體概念,這邊以常見的「外部裝置(也就是文件)」、「內核」映射為例

A. 文件映射區

● 文件是外部硬體提供,而 Linux 會將文件先讀取到核心 (Kernel) 中,在透過記憶體映射的方式,映射到目標應域的 RAM 中 (虛擬記憶體上)

如此設計的原因是因為加快訪問速度,因為外部裝置的速度是最慢的

● 應用程序在操縱文件時,其實就是在操縱 Kernel 映射到當前 Process 的記憶體區塊

可參考 檔案映射 - mmap 動態拓展 文章,該文章有做小實驗

B. 內核映射區:將內核 (Kernel) 映射到應用記憶體中

● 每個應用進程都存活在獨立的進程空間中,每個進程有 0 ~ 3G 的使用者記憶體空間、1G 的內核空間 (這記憶體空間可能並非連續,這使用到了 虛擬記憶體技術)

這裡的虛擬內存量,是以 32 位元系統為例

auto - 修飾局部變數

auto 關鍵字在 C 語言中,唯一的功能就是 修飾局部變數;表示該變量是自動局部變量 (依樣分配在 Stack 上)

既然分配在 Stack 上,代表部初始化,其 Value 就是隨機的

平時在定義局部變量時只是省略個 auto 關鍵字,並且 auto 不可以使用在全局變量

auto 也會自動推導,目前使用的區域變數類型 (沒定義類型的話會警告,但仍可編譯成功)


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);
}

static - 靜態變量

static 關鍵字在 C 語言中有兩種用法,但這兩種用法沒相關,個代表了不同的意思

A. 修飾 局部變數透過 static 關鍵字修飾,該變量的儲存位置就不再是 Stack,也就是說該變量的數值會一直存在 !

● 已定義初始化:存在 .data

● 未定義初始化:存在 .bss


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;
}

B. 修飾 全域變數:這有關於多檔案的鏈結,使用 static 描述的變數、函數,都不可以被其他檔案使用,只能在內部使用

鏈結時再說明

register - 提高變數讀寫

register 簡單來說就是要求編譯器將變數放置到 CPU 等級的 Register 做存取(當然,這並不一定全部被的變數都會被放置到 Register 中)

一般沒有聲明的變量是規劃在 RAM 記憶體空間

register 使用起來跟一般變量一樣


void register_test() {
    register int a = 100;    // 規劃到棧存器
    int b = 200;            // 規劃到 RAM


    printf("a: %d\n", a);
    printf("b: %d\n\n", b);
}

並非一定安排的到暫存器,畢竟 CPU 棧存器數量有限制,所以需慎用

extern - 跨文件訪問

● 我們知道 C 語言是以文件為單位來做編譯,如果要跨文件使用變量、函數,就需要使用 extern 關鍵字來修飾 (只能修飾全局變量)

● 這裡要注意一件事… 定義宣告 兩者個差異

宣告:僅是告訴編譯器,有一個 符號

定義:同時有宣告的意義,並加上為該符號 在記憶體中佔(耗費)一個位置


// storage.c
int var = 10;        // 宣告 + 定義

// ------------------------------------------------------
// extern_test.c

extern int var;    // 宣告

void test_extern() {
    var += 10;

    printf("extern val: %d\n", var);
}

● 不需要再使用 #include "storage.c",因為 extern 會去全局 (整個應用) 中尋找相對應的符號

volatile - 易變

使用 volatile 關鍵字有以下特點

變量由外部修改:最常見的就是在中斷時改變變量數值,其次還有,在 mutli thread 時修改數值,硬體修改數值... 等等

寫入相同數值時:不會再對記憶體寫入,使用 volatile 修飾變量後,就算是同樣的數值也會寫入


int a = 10;
a = 10;        // 由於是相同數值,所以會「被編譯器優化」
a = 10;
a = 10;
a = 10;
a = 10;

volatile 就是告訴編譯器不要隨意進行優化,使用方式如下


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 - 限制

在 C99 之後才出現

gcc 編譯可以使用 -std=c99 來指定編譯版本

restrict 關鍵字是 用於限制、約束 指標,目的是為了讓編譯器更好的優化

簡單來說,在範圍內,修飾的指標不會被參考


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);
}

typedef - 定義新類型

● typedef 在 C 中是屬於 儲存類,所以 不可用第二個儲存類型關鍵字修飾

● 使用請參考另一篇 指標 & Array & typedef


C 語言 - 作用域

作用域的重點在 {} 符號 (ifwhilefor... 都有),只要變量一進入該符號就開始生命,離開就結束生命週期

作用域 - 區域變量

● 相同符號是可以內蓋外


#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;
}


C 語言- 程式生命週期

生命週期

● 開始:運行中分配變量空間,排斥其他變量操控

● 結束:回收變量空間,其他變量可用

Stack、Heap

A. Stack 跟 作用域 相當有關,生在作用域內,一離開作用域就死亡


void my_stack() {
    int a = 10;    // a 被規劃

    for(int b = 0; b < = 20; b++) {    // b 被規劃
        a += b;
    }
    // b 結束生命週期
    
    printf("a: %d\n", a);
    
} // a 結束生命週期

B. Heap 生命週期是使用者手動管理,生命週期從 malloc ~ free 之間,可以跨 Function,因為它的記憶體分配在 Heap 上,而非 Stack 上


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 - 全局變量

操作不慎可能導致 OOM or 記憶體洩漏

.data & .bss 用來描述全局變量,並且它們的 生命周期是永久 (與應用同進退)

● 至於其他段 .text & .rodata 也是生命周期永久

函數 & Stack

● 函數有幾個特點

A. 入參建議不要超過 4 個,超過建議使用 struct 包裹起來(或是使用指標)

否則可能會造成 stack 的負擔

B. 傳入參數大小也不建議過大,避免超過 Stack Size,較大參數建議使用 Pointer 傳遞

C. 編譯完後函數 會存在 elf 中的 .text

● 可以使用 readelf 指令查看

readelf -S Hello.out

函數 - 入棧 Stack

● 函數的入棧是按照順序的,如下面程式的入棧順序就是

● Stack 棧是由核心棧存器 SP 來管控,相關的核心棧存器還有 鏈結 LR、計數器 PC

A. main 函數返回地址

B. main 函數

C. 遇到 subtraction 函數,保存 subtraction 函數完成後的返回地址

D. subtraction 函數入棧,這裡又細分為,參數由右到左入棧


int subtraction(int a, int b) {
    return a - b;
}

int main(void) {
    int result = subtraction(3, 5);    //    Error  呼叫不到 subtraction 函數
}

Stack 概念圖如下


C 語言 - 鏈結檔案

鏈結:鏈結在 C 語言中主要就是在鏈結個個檔案的匯編結果 (.o),也就是將各個獨立的二進為檔鏈結,形成一個可執行檔;

編譯以文件為單位,鏈結以工程應用為單位

鏈結屬性

● C 語言中的鏈結有三種屬性

A. 外鏈結使用 extern 修飾的全局變量函數 都屬於外鏈結部分


extern int a;

void printA() {
    printf("A: %d\n", a);
}

● 這在大型專案中容易會有重名的問題

B. 內鏈結:使用 static 修飾的函數、全局 static 變量 (不包含局部 static)


static int count = 10;

static void _cal_val(int *const val) {
    *val = *val + 100;
}

static 可以解決部分函數、變量重名的問題;static 修飾過後即便其他檔案有,也不會相互衝突

C. 無鏈結局部變量、auto 修飾、局部靜態變量


void my_local() {
    int a = 10;
    auto b = "Hello";

    static c = 200;
}

更多的 C 語言相關文章

關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言

編譯器、系統開念

編譯器、系統開念:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節

C 語言與系統開發

C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響