未定義行為與安全

讀取未初始化的變數

當你撕下一張便條紙來塗寫時,那張紙並不是空白的——上面還留著之前印或寫過的東西。在 C 裡宣告一個區域變數而不給它值,就像拿到這樣一張紙:這個變數佔用的記憶體本來就裝著某些東西,如果你在賦值之前就使用它,讀到的就是那些殘留內容。初學者常以為新變數從零開始。對區域變數而言,並不會。

精確地說:你宣告但未初始化的區域(自動)變數帶有一個不確定值,而讀取它在多數情況下是未定義行為。人們很容易以為這只是給你一個「隨機數」,但其實更糟——因為它是未定義行為,編譯器可以假設這次讀取永不發生並據此最佳化,所以結果甚至不保證是個一致的殘留值;同一個變數在不同次讀取時可能看起來裝著不同的東西。有些誠實的例外:具靜態生存期的變數(全域變數,以及宣告為 static 的區域變數)「會」被標準零初始化,而 calloc 取得的記憶體也是清零的。但 malloc「不會」清零它的記憶體,堆疊上的普通區域變數也不會清零——所以 'int count;' 接著 'count++;' 讀到的是垃圾值。

為什麼重要:未初始化的讀取造成的臭蟲令人抓狂,因為它們是非決定性的——程式在除錯建構(有時會清零記憶體)裡可能正常,在發行建構裡卻失敗,或是這一次跑得好、下一次卻當掉。紀律既簡單又便宜:在宣告每個變數的同時就初始化它,哪怕只是給個無害的預設值像 0 或 NULL。開啟警告後(gcc -Wall -Wextra)編譯器會對許多這類讀取提出警告,而 MemorySanitizer 或 Valgrind 這類工具能抓出編譯器漏掉的。

int count; /* 區域變數,未清零 */ count++; /* 讀到不確定值:UB */ int total = 0; /* 修正:宣告時就初始化 */ static int g; /* 靜態生存期:保證為 0 */ int *p = malloc(4); /* malloc 不清零;*p 是垃圾值 */ int *q = calloc(1, 4); /* calloc 會清零;*q 是 0 */

區域變數與 malloc 取得的記憶體不會清零;靜態生存期變數與 calloc 取得的記憶體會。請在宣告時初始化區域變數。

新的區域變數並非零——只有靜態生存期變數與 calloc 保證為零。更糟的是,讀取不確定值是 UB,所以它甚至不是穩定的「隨機」數;編譯器每次讀取可能給出不同結果。

又稱
indeterminate valuegarbage value未初始化變數不確定值