Overview of Content
本文旨在提供對程式記憶體管理與編譯器規劃的入門理解
我們將首先介紹程式記憶體分配的基本概念,包括程式記憶體管理方式和區域規劃,以及函數使用中的棧(Stack
)和堆(Heap
)分配。
接著,將探討標準函數 malloc
的基本細節,以及編譯器規劃中的 Segment 段
,包括 程式段
、數據段
、bss段(ZI
)、以及 rodata 段
此外,我們還將簡要討論字符串的定義方式、特性,以及 sizeof
與 strlen
之間的基本差異。透過本文,讀者將建立對這些關鍵概念的初步了解,有助於奠定程式開發與優化的基礎。
記憶體又稱為內存,以下會混用
程式記憶體分配 - 概述
在 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
● 字符中的
\0
、0
的差別,我們把字符中與 ASCII 編碼對照,就可以插出兩者的差別
字符 ASCII 編碼 \0
0 0
48
字符串 - 定義方式
● 字符串定義方式有兩種:
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 語言與系統開發
● C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用