回傳碼 vs errno(the return code versus errno)
/ errno: "ERR-no" /
想像一台自動販賣機,出問題時會亮起單一一盞紅色的「失敗」燈,並另外印出一張小收據,寫明究竟哪裡出錯——「沒有零錢」「卡貨」「缺貨」。那盞紅燈讀起來很快,告訴你「是或否」;收據則承載細節。C 經典的錯誤回報風格正是如此:函式的回傳值是那盞燈,而一個名叫 errno 的獨立全域變數則是那張收據。
這個慣例實際上是這樣運作的。許多 C 函式庫與系統呼叫函式以回傳值來示意失敗:open() 失敗時回傳 -1、malloc() 回傳 NULL、fopen() 回傳 NULL、read() 失敗時回傳 -1。那個回傳值通常只告訴你「它失敗了」,而不告訴你「為什麼」。要知道原因,就讀 errno——一個失敗的呼叫會把它設成某個數值錯誤碼的特殊變數,例如 ENOENT(沒有這個檔案)或 EACCES(權限不足)。規矩是:先檢查回傳值看它到底有沒有失敗;唯有失敗了,errno 才有意義。一條關鍵又容易漏掉的規則是:成功的呼叫不會把 errno 歸零,而後續不相干的呼叫可能會覆蓋它,所以你必須在一個「已知失敗」的呼叫之後立刻讀 errno——絕不要光看 errno 來判斷某件事是否失敗。
為何重要:這種兩段式拆分(回傳值說「是/否」,errno 說「是哪一種」)是 C 與 POSIX 中最主流的錯誤回報風格,你到處都會看到它。它強大卻脆弱:errno 是共享狀態(現代系統中是「每執行緒一份」,這避開了一整類錯誤),而人們常忘記它只在「已知失敗」之後才有效。較新的語言以更豐富的回傳型別——Result 或 Option——取代這個模式,正是因為一個獨立的全域錯誤變數實在太容易被誤用。
正確用法:FILE *f = fopen("data.txt", "r"); if (f == NULL) { perror("fopen"); /* perror() 會把 errno 轉成「fopen: No such file or directory」 */ }——先檢查回傳值(NULL);errno(透過 perror)只在我們已知它失敗時才去讀。
用回傳值判斷「是否」失敗;用 errno 得知「為什麼」失敗。
別在未經回傳值確認失敗之前就去測 errno。一個成功的呼叫可能讓 errno 保留著先前某次失敗留下的舊值;errno 只在「已知失敗」的呼叫之後立刻才有意義。