錯誤處理與穩健性

哨符回傳值(sentinel return value)

/ sentinel: "SEN-tih-nuhl" /

想像一支溫度計,恰好讀到 -999 並不代表某個溫度——它代表「感測器壞了,別理我」。那個特別的、不可能成為真實答案的值,就是哨符:一個被保留來代表「失敗」而非正常結果的特定回傳值。C 函式不斷地用哨符,把「成功了嗎?」和「答案在這裡」塞進單一一個回傳的數字裡。

其想法是挑一個絕不可能成為合法成功結果的值,並把它保留給失敗。標準的選擇遵循幾個模式:回傳指標的函式以 NULL 代表失敗(malloc()、fopen()、找不到時的 strchr());回傳計數或描述符的函式以 -1 代表失敗(open()、read()、write()——成功時回傳非負,所以 -1 安全地落在範圍外);回傳小型狀態的函式以 0 代表成功、非 0 代表失敗(和布林相反,這常絆倒初學者)。呼叫者就和哨符比對:if (p == NULL)、if (fd == -1)、if (rc != 0)。這就是成功/失敗回傳慣用法——一個值身兼兩職,既示意結果又承載資料。

為何重要,以及它在哪裡反咬:哨符便宜又無所不在,但它有個真實的缺陷——它縮小了合法答案的集合。經典陷阱是:某個函式的整個輸出範圍都有意義,沒有多餘的值能拿來當哨符。教科書案例是 getchar():它回傳字元,但為了同時示意檔案結尾,它必須回傳 EOF,這正是為什麼 getchar() 回傳 int 而非 char——它需要在每一個可能的位元組之外多一個值。另一個陷阱是「過載」:如果 -1 既是哨符、又是某個可能的真實結果,你光靠回傳值就分不出它們,這正是逼出「回傳值加 errno」拆分的情境。較豐富的語言以一個獨立型別(Option、Result)解決這點,讓成功與失敗兩種情況永遠不會彼此混淆。

int c; while ((c = getchar()) != EOF) { putchar(c); }——getchar() 把每個位元組以非負的 int 回傳,並以哨符 EOF(一個負值)代表檔案結尾。把它存進 char c 而非 int c 會是個錯誤:值為 0xFF 的位元組可能被誤認為 EOF,或永遠偵測不到 EOF。

EOF 是個落在任何真實位元組範圍之外的哨符,這正是 getchar() 回傳 int 的原因。

哨符只在「失敗值絕不可能是合法結果」時才管用。當每個輸出都有意義時,你就需要更寬的型別(像 getchar 用 int)或一個獨立通道(回傳值加 errno),否則就有把真實答案誤當成「失敗」的風險。

又称
error sentinelmagic error valueout-of-band return value標記值