平常在嵌入式系統上用 malloc,
寫完之後覺得很爽,動態配置記憶體,好像很厲害。
uint8_t *buf = malloc(1024);
memset(buf, 0, 1024);
// 開始用 buf...
有一次朋友看了一眼問我:「malloc 失敗怎麼辦?」
我說:「會失敗嗎?記憶體應該夠吧?」
他說:「嵌入式的 heap 就那麼大,你確定嗎?」
我 ........ 當然不是很確定。
malloc 可能會失敗
這件事很多人知道,但寫程式的當下常常又神奇的忘記。
malloc 在以下情況會失敗,回傳 NULL:
- heap 空間不足 — 嵌入式系統的 heap 通常只有幾 KB 到幾十 KB
- heap 碎片化 — 有足夠的總空間,但沒有連續的大塊可以分配
- 請求太大 — 一次要求超過可用空間
// malloc 失敗時回傳 NULL
uint8_t *buf = malloc(1024);
// ❌ 沒有檢查,直接用
memset(buf, 0, 1024); // buf 是 NULL,這裡直接 crash
在 PC 上,記憶體通常很充裕,
malloc 失敗的機率很低,
所以很多人養成了不檢查的習慣。
帶到嵌入式,就踩坑了。
基本的防禦寫法
uint8_t *buf = malloc(1024);
if (buf == NULL) {
// 配置失敗,處理錯誤
LOG_ERROR("malloc failed, size=%d", 1024);
return ERR_NO_MEMORY;
}
// 確認 buf 有效才繼續
memset(buf, 0, 1024);
// ... 使用 buf ...
就這樣,多三行,但可以救你很多次。
包裝成安全版本
每次 malloc 都要寫一樣的檢查,很囉嗦。
我習慣包一個 wrapper:
// safe_malloc.h
#include <stdlib.h>
#include <string.h>
/**
* 配置記憶體並清零,失敗時回傳 NULL
*/
static inline void *safe_malloc(size_t size) {
if (size == 0) {
return NULL;
}
void *ptr = malloc(size);
if (ptr != NULL) {
memset(ptr, 0, size); // 順便清零,避免垃圾值
}
return ptr;
}
/**
* 配置記憶體,失敗時印 log 並回傳 NULL
*/
#define MALLOC_LOG(size) malloc_with_log((size), __FILE__, __LINE__)
static inline void *malloc_with_log(size_t size,
const char *file,
int line) {
void *ptr = malloc(size);
if (ptr == NULL) {
// 你的 log 函式
log_error("[%s:%d] malloc failed, size=%zu", file, line, size);
}
return ptr;
}
使用:
uint8_t *buf = safe_malloc(1024);
if (buf == NULL) {
return ERR_NO_MEMORY;
}
或是想要自動帶 log:
uint8_t *buf = MALLOC_LOG(1024);
if (buf == NULL) {
return ERR_NO_MEMORY;
}
calloc vs malloc
很多人只知道 malloc,但 calloc 在某些情況更好用:
// malloc:配置記憶體,內容是垃圾值
uint8_t *buf = malloc(1024);
// calloc:配置記憶體,內容清零
// calloc(元素數量, 每個元素大小)
uint8_t *buf = calloc(1024, sizeof(uint8_t));
calloc 幫你做了清零,不需要再 memset。
另外,calloc 在計算總大小的時候,
會幫你做溢位檢查:
// 如果 count * size 溢位,malloc 可能配置到錯誤大小的記憶體
size_t count = 1000000;
size_t size = 10000;
uint8_t *buf = malloc(count * size); // count * size 可能溢位!
// calloc 會在內部檢查,溢位時回傳 NULL
uint8_t *buf = calloc(count, size); // 安全
我的習慣:
- 需要清零的用
calloc - 不需要清零(之後會完整寫入)的用
malloc
realloc 的陷阱
realloc 是最容易踩坑的:
// ❌ 經典錯誤寫法
buf = realloc(buf, new_size);
if (buf == NULL) {
// realloc 失敗了
// 但原本的 buf 已經被 free 掉了!
// 這裡發生了記憶體洩漏
return ERR_NO_MEMORY;
}
realloc 失敗的時候,原本的記憶體不會自動釋放,
但如果你直接把回傳值存回 buf,
原本的指標就丟失了,記憶體就洩漏了。
// ✅ 正確寫法
void *new_buf = realloc(buf, new_size);
if (new_buf == NULL) {
// realloc 失敗,buf 還是有效的
LOG_ERROR("realloc failed, new_size=%zu", new_size);
// 你可以選擇繼續用原本的 buf,或是 free 掉它
free(buf);
buf = NULL;
return ERR_NO_MEMORY;
}
buf = new_buf; // 成功才更新 buf
用一個暫時的指標接 realloc 的回傳值,
失敗的時候原本的 buf 還在,你可以決定怎麼處理。
嵌入式:該不該用動態記憶體?
這個問題在嵌入式社群裡有很多討論。
有些嚴格的嵌入式規範(像是 MISRA-C)
禁止在執行時期使用動態記憶體配置,
原因是:
- 不可預測性 — 不知道什麼時候會失敗
- 碎片化 — 長時間執行後 heap 可能碎片化,導致配置失敗
- 即時性 —
malloc的執行時間不固定,對 RTOS 的即時任務有影響 - 難以測試 — 記憶體不足的情況很難在測試中重現
航太、醫療、汽車等安全關鍵系統,
通常完全禁止動態記憶體配置。
替代方案:靜態記憶體池
如果需要「動態」的感覺,但又不想用 heap,
可以自己做一個靜態的記憶體池:
#define POOL_BLOCK_SIZE 256
#define POOL_BLOCK_COUNT 8
typedef struct {
uint8_t data[POOL_BLOCK_SIZE];
bool in_use;
} MemBlock;
static MemBlock s_pool[POOL_BLOCK_COUNT];
void *pool_alloc(void) {
for (int i = 0; i < POOL_BLOCK_COUNT; i++) {
if (!s_pool[i].in_use) {
s_pool[i].in_use = true;
memset(s_pool[i].data, 0, POOL_BLOCK_SIZE);
return s_pool[i].data;
}
}
LOG_ERROR("memory pool exhausted");
return NULL;
}
void pool_free(void *ptr) {
for (int i = 0; i < POOL_BLOCK_COUNT; i++) {
if (s_pool[i].data == ptr) {
s_pool[i].in_use = false;
return;
}
}
LOG_ERROR("pool_free: invalid pointer");
}
這樣做的好處:
- 記憶體在編譯時期就確定了,不會有碎片化
- 配置和釋放的時間是固定的
- 可以監控使用量,知道最多同時用了幾個 block
缺點是彈性比較低,block 大小固定,
不適合需要各種大小的情況。
說實話
在嵌入式,我對 malloc 的態度是:
能不用就不用,要用就要用對。
很多情況下,靜態配置就夠了:
// 不用 malloc,直接靜態配置
static uint8_t rx_buf[RX_BUF_SIZE];
static uint8_t tx_buf[TX_BUF_SIZE];
記憶體在編譯時期就確定,不會有配置失敗的問題,
也不需要管理生命週期。
但如果真的需要動態配置,
至少要做到:
- 每次
malloc之後都檢查NULL free之後立刻清空指標(上一篇說過)- 有辦法監控 heap 使用量
第三點很多人忽略,
但在嵌入式,知道 heap 用了多少,
可以幫你在問題發生之前就發現異常。
這篇的 Checklist
- [ ] 每次
malloc/calloc之後都有檢查NULL - [ ]
realloc有用暫時指標接回傳值,不是直接覆蓋原指標 - [ ]
free之後有清空指標(= NULL) - [ ] 需要清零的地方用
calloc取代malloc+memset - [ ] 評估過是否真的需要動態記憶體,靜態配置是否可行
- [ ] 有辦法監控 heap 使用量(嵌入式)
- [ ] 長時間執行的系統有考慮 heap 碎片化問題