C 語言記憶體區塊規劃 | Segment 段 | 字符串特性

C 語言記憶體區塊規劃 | Segment 段 | 字符串特性

Overview of Content

本文旨在提供對程式記憶體管理與編譯器規劃的入門理解

我們將首先介紹程式記憶體分配的基本概念,包括程式記憶體管理方式和區域規劃,以及函數使用中的棧(Stack)和堆(Heap)分配。

接著,將探討標準函數 malloc 的基本細節,以及編譯器規劃中的 Segment 段,包括 程式段數據段bss段(ZI)、以及 rodata 段

此外,我們還將簡要討論字符串的定義方式、特性,以及 sizeofstrlen 之間的基本差異。透過本文,讀者將建立對這些關鍵概念的初步了解,有助於奠定程式開發與優化的基礎。

記憶體又稱為內存,以下會混用


程式記憶體分配 - 概述

在 PC 作業系統中的軟體 (非 RTOS),因為有 MMU (Memory Manager Unit) 硬體的關係,所以軟體實際取得的記憶體位置時大多數 都是虛擬記憶體位置

這樣的好處是可以 更好的利用 規劃虛擬記憶體 空間

有關虛擬記憶體,可以參考 Linux 內存管理 & 操作 這篇

程式記憶體管理方式/規劃區域

● 編譯器對程式編譯後,會將程式碼分析並區分為幾個區域:主要有分為 數據區常量區 幾類,儲存區預如下表

每個區域都會有不同的特性

稱呼記憶體區塊規劃名稱
stack
heap
數據區.data.bass
常量區.ro.data

函數使用: 棧 Stack

● 在區域變數中使用,並有以下特點

A. 自動回收:進入函數之前自動分配,離開函數時自動釋放

B. 反覆利用:Stack 在 RAM 中的一個空間中,而該空間會不斷重複利用

C. 髒內存:釋放的內存空間中的舊數據並不會配清除

D. 臨時性:函數不能返回區域變數內的地址 (例如某一個指標),因為該空間在函數離開後自動釋放


#include <stdio.h>
#include <stdlib.h>

int *getTestValue() {
    int value = 100;
    int *p = &value;

    return p;    // 返回區域變數很危險
}

● 雖然上面這段程式可能成功了,但是十分危險

堆 Heap - malloc 使用

malloc/free 是 C 語言提供的標準庫的 API 之一:可以用來動態申請、釋放記憶體,可在區域、全域變數使用 malloc 函數做動態記憶體申請,它的特點如下:

A. 靈活:可在運行時臨時申請

B. 內存量大:進程可以按照需要使用 C stander library 提供的 malloc api 手動申請記憶體空間空間

C. 必須手動申請、釋放記憶體空間

如果不釋放,則該空間會持續佔據應用中的記憶體空間,最終導致記憶體洩漏

D. 髒內存:釋放的記憶體空間內的資料並不會被立刻清除,可能會遺留

E. 臨時性:生命週期只存在 申請 ~ 釋放之間,釋放後再取相同地址會得到不可預期的結果

● 其實 malloc 是由 mmap 而來,並且會由標準庫來負責管理申請的記憶體


#include <stdio.h>
#include <stdlib.h>

void dynamic_memory() {
    int *p = (int *)malloc(1000 * sizeof(int));

    if(p == NULL) {
        printf("malloc memory fault.");
        return;
    }

    *(p + 1) = 123;
    *(p + 2) = 456;
    printf("Use memory p1: %d, p2: %d\n", *(p + 1), *(p + 2));

    free(p);
    // 會出現無法預期的錯誤
    // 最好是在 free 之後,將 p 置為 NULL
    printf("After free ptr, Use memory p1: %d, p2: %d\n", *(p + 1), *(p + 2));
}

記憶體 (內存) 洩漏 ?

內存在 RAM 中佔有一塊位置,但並無任何程式在使用這塊 RAM 空間,這就是內存洩漏

標準函數 malloc 細節

malloc 函數是 C 語言的標準函是庫提供,在使用時須重義一些特點

A. malloc 函數 返回值為 void* 指標:該空間存放的數據類型尚未確定,也就是 可以存放任何類型的數據,而要存放啥數據等待使用者強制轉型

可以解為:類似於 Java 的 Object 類的角色

B. 動態申請記憶體結果:如果該函數成功則返回第一個 Byte 的地址;失敗則返回 NULL

C. 釋放記憶體:使用成對的 free 函數就可以釋放,不過一定要記得使用原來給予的地址做釋放

● 對於使用 malloc 函數申請的空間;申請 0 空間 返回結果不確定,有可能反為 NULL,須看函數庫如何實現


void malloc_0_size() {
    int *p = (int *)malloc(0);

    if(p == NULL) {
        printf("malloc size 0 fault.\n");
        return;
    }
    printf("malloc size 0 success: %p, size: %d.\n", p, sizeof(*p));

    *p = 0x7FFFFFFF;
    printf("Value: %d.\n", *p);
}

雖然範例是申請成功的,但建議還是不要這樣使用


編譯器規劃:Segment 段

編譯器在編譯程式時,會按照一定的結構、規則拆分程式,組成不同的段;常見的段就有 .text.bss.data

.text 程式段

.text 是編譯器分析過後,專門放置程式的位置,關於程式的指令也就放在這


// .text
void sample_func(int v) {
    char *s = NULL;

    if(v == 0) {    // 判斷指令放在 .text
        s = "Hello";
    } else {
        s = "World";
    }

    printf("%s\n", s);
}

.data 數據段

.text 是編譯器分析過後,專門放置數據的位置,同時也稱為 靜態數據區靜態區


// 放置在 .txt 段    

static char* HELLO = "HELLO~";

void my_function() {
    static int a = 10;    // 分配在 .data 段

    int b = 100;    // 分配在 Stack
}

● 與 Heap 幾乎相同,但是 .data 段生命週期是一直到程式結束才會結束

.bss 段 / ZI

.bss 段又稱為 ZI (Zero Initial) 段,所有為顯示初始化的 靜態 (全局) 變量 都放置在此,這個段會自動將 尚未初始化 的靜態空間初始化為 0 (或顯示初始化為 0 的也會放置在這)


// 放置在 .bss 段

char* WORLD;

int count = 0;

● 全局變量如果 已經初始化,則會放置到 .text 區塊

.rodata 段

.rodata 段是用來放置「常量數據」 (也就是程式中的不可修改的數據)

● 至於使用關鍵字 const 修飾過後是否放置在 .rodata 段呢?

只能說可能,這要依據每個平台的實現 const 的方式來決定

字符串位置 - .text/.data

字符串位置

A. 字符串也可能放到 .text:單晶片的編譯器較常這樣實現;有另外一種可能是放置到 .data

B. 字符串也可能放到 .data:GCC 就是這樣實現字符串


void ptr_str() {
    char *p = "My C";    // 放置在 `.data`

    printf("%s\n", p);
}

C 語言:字符串

其他語言像是 C、Java、C#... 都有 string 這個關鍵字來描述字符串,而 C 語言中沒有,C 語言反而是使用指標 提供一連串的字符 首地址


// c 表達字符串的方式
char *c = "Hello String";

字符串特性

● 上面說了,字符串其實就是指向一連串字符空間的首地址

● C 語言使用 ASCII 對字符串便碼,編碼後就可以使用 char 將編碼存起來

● C 語言的字符串特點如下

A. 只提供使用者 第一個字符的地址

B. 字符串尾部固定是 \0:這個 \0 是所謂的 魔數,在很多程式中都有,魔數個代表了不同的意義

字符串中就不可以有 \0 這個符號,否則會分不清是否是結尾

C. 字符的地址是連續 的,如同 Array

字符中的 \00 的差別,我們把字符中與 ASCII 編碼對照,就可以插出兩者的差別

字符ASCII 編碼
\00
048

字符串 - 定義方式

字符串定義方式有兩種:

A. 使用指標 char* 定義字符串

字符串的大小:字符串大小 + \0 + 指針大小

\0 上面我們已經得知這是必要的規則,所以占用 1 個 Byte

指標大小:其實我們所取得的 並不是第一個 char 的地址,而是取得指向第一個 char 地址的指針,如下圖


void test_string_ptr() {
    char *p = "Hello";

    printf("p: %p,&p: %p\n", p, &p);
}

下圖 004063EC H 字符的地址;0064FF0C 是 p 的地址

B. 使用陣列 Array 定義字符串

字符串的大小\0 + 指針大小

● 我們可以看到 Array、Ptr 所定義的 string 是不同的;Array 所定義的 String,其符號是指向第一個 char 字節地址,所以 Array 定義的 String 可以修改


void test_string_array() {
    char p[] = "Linux";

    printf("p: %p,&p: %p, %s\n", p, &p, p);

    p[0] = 'G';
    printf("p: %p,&p: %p, %s\n", p, &p, p);
}

● Ptr & Array 定義字符串差異

sizeof & strlen 差異

sizeof 是 C 語言的 「運算符」,不是函數strlen 則是標準函數庫所提供的函數,專門用來計算字符串長度

sizeof 會計算包括 \0 的長度


void string_len() {
    char p[] = "HelloWorld";

    int len = sizeof(p);
    printf("string length: %d\n", len);

    len = strlen(p);
    printf("string length: %d\n", len);

    char *p2 = "HelloWorld";
    len = strlen(p2);
    printf("string length: %d\n", len);
}

strlen 函數在使用時,傳入的必須是指標,並且該指標一定要指向一個字符串,否則沒有意義


void err_use_strlen() {
    int apple = 10;
    int *p = &apple;

    int len = strlen(p);        // 傳入非字串指標,沒意義!
    printf("string length: %d\n", len);
}

更多的 C 語言相關文章

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

編譯器、系統開念

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

C 語言與系統開發

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

Leave a Comment

Comments

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

發表迴響