剛學 C 的時候,老師說過:「變數要記得初始化。」
我點點頭,然後還是繼續寫:
int count;
char buf[64];
int *ptr;
反正當下程式跑起來好像也沒問題。
直到有一天,發現程式出現一個「偶發性」的奇怪行為。
有時候正常,有時候不正常,完全沒有規律。
Debug 了兩天,最後發現是一個沒有初始化的變數,
裡面的值取決於上次那塊記憶體被誰用過、放了什麼東西。
在開發機上跑都正常,因為記憶體的初始狀態剛好是 0。
換到客戶的環境,跑幾個小時之後記憶體狀態不一樣了,就爆了。
那次之後,我對「初始化」這件事認真多了。
未初始化的變數裡面是什麼?
答案是:不知道。
C 語言規格說,讀取未初始化的自動變數(local variable)是 undefined behavior。
實際上,那塊 stack 記憶體裡放的是「上一次被使用時留下的值」,
也就是俗稱的垃圾值(garbage value)。
void foo(void) {
int x;
printf("%d\n", x); // 可能是 0,可能是 32767,可能是任何值
}
在你的開發環境跑,可能每次都印出 0,
因為這塊 stack 剛好沒被動過。
但這不代表程式是對的,只是剛好沒出事。
三種記憶體區域,行為不一樣
這裡要稍微分一下,不同區域的變數,初始化行為不同:
Global / Static 變數 — 自動初始化為 0
int global_count; // 保證是 0
static int module_state; // 保證是 0
void foo(void) {
static int call_count; // 保證是 0(只有第一次)
}
這些變數放在 BSS segment,程式啟動時會被清零。
所以就算你沒有明確初始化,值也是 0。
但我還是建議明確寫出來:
int global_count = 0;
static int module_state = STATE_IDLE;
原因是讓讀程式碼的人知道你是故意給 0,不是忘記初始化。
這兩件事看起來一樣,但意圖完全不同。
Local 變數 — 不會自動初始化
void foo(void) {
int count; // 垃圾值
char buf[64]; // 64 個垃圾值
int *ptr; // 可能指向任何地方
}
這些放在 stack,不會自動清零。
一定要自己初始化。
void foo(void) {
int count = 0;
char buf[64];
memset(buf, 0, sizeof(buf));
int *ptr = NULL;
}
Heap 記憶體 — 看你用哪個函式
// malloc:不初始化,裡面是垃圾值
int *arr = malloc(10 * sizeof(int));
// calloc:初始化為 0
int *arr = calloc(10, sizeof(int));
用 malloc 之後如果沒有初始化就直接使用,
跟 local 變數一樣,是 undefined behavior。
嵌入式的特別狀況
在嵌入式開發,這個問題更微妙。
Reset 之後不一定是乾淨的
某些 MCU 在 warm reset(軟體重置)之後,
RAM 的內容不會被清除,還是上次執行時的值。
如果你的程式依賴「reset 之後變數是 0」這個假設,
在 cold boot(重新上電)正常,warm reset 就可能出問題。
// 這個假設在 warm reset 後可能是錯的
static int error_count; // 以為是 0,其實是上次的值
解法是明確初始化,或是在 startup code 裡確保 BSS 被清零。
Struct 的初始化
Struct 很容易漏掉某些欄位:
typedef struct {
uint8_t device_id;
uint16_t timeout_ms;
bool is_enabled;
uint8_t retry_count;
} DeviceConfig;
// ❌ 只初始化部分欄位,其他是垃圾值
DeviceConfig cfg;
cfg.device_id = 1;
cfg.is_enabled = true;
// timeout_ms 和 retry_count 是垃圾值!
我的習慣是先用 memset 清零,再設定需要的值:
// ✅
DeviceConfig cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.device_id = 1;
cfg.is_enabled = true;
// timeout_ms = 0, retry_count = 0,至少是已知的值
或是用 C99 的 designated initializer,更清楚:
// ✅ C99 寫法,沒有列出的欄位自動是 0
DeviceConfig cfg = {
.device_id = 1,
.is_enabled = true,
};
這個寫法我很喜歡,一眼就知道哪些欄位是刻意設定的,
哪些是用預設值(0)。
指標一定要初始化為 NULL
未初始化的指標是最危險的:
// ❌
uint8_t *data_buf;
if (some_condition) {
data_buf = malloc(256);
}
// 如果 some_condition 是 false,data_buf 是垃圾值
// 這裡 dereference 就是 undefined behavior,通常直接 crash
process_data(data_buf);
// ✅
uint8_t *data_buf = NULL;
if (some_condition) {
data_buf = malloc(256);
}
if (data_buf != NULL) {
process_data(data_buf);
}
初始化為 NULL 之後,至少你可以檢查它,
不會在不知情的情況下存取到隨機的記憶體位置。
編譯器可以幫你抓
開啟警告之後,編譯器通常可以抓到明顯的未初始化使用:
gcc -Wall -Wextra -Wuninitialized your_code.c
warning: 'count' is used uninitialized [-Wuninitialized]
但編譯器不是萬能的,複雜的控制流程它不一定抓得到:
int result;
if (condition_a) {
result = compute_a();
} else if (condition_b) {
result = compute_b();
}
// 如果兩個條件都不成立,result 是未初始化的
// 某些情況下編譯器不會警告
return result; // 潛在的問題
所以還是要養成習慣,不能完全依賴編譯器。
說實話
初始化這件事,說起來很簡單,做起來需要一點紀律。
趕進度的時候,宣告完變數就直接用,
反正在自己機器上跑都正常。
但嵌入式產品的問題是,你的開發環境和客戶的環境不一樣,
記憶體狀態不一樣,跑的時間不一樣,溫度不一樣。
那種「在我這裡沒問題」的 bug,
通常都是某個地方依賴了未定義的行為。
多打幾個 = 0、= NULL、memset,
真的可以省掉很多不必要的 debug 時間。
這篇的 Checklist
- [ ] Local 變數宣告時有明確初始化
- [ ] 指標初始化為
NULL,不是讓它指向隨機位置 - [ ]
malloc之後有初始化記憶體內容(或改用calloc) - [ ] Struct 初始化有用
memset或 designated initializer - [ ] Global / static 變數有明確寫出初始值,表示是故意的
- [ ] 複雜控制流程的變數,確認所有路徑都有初始化
- [ ] 開啟編譯器的
-Wuninitialized警告