錯誤處理與穩健性

防禦性複製與引數驗證(defensive copying and validating arguments)

想像一位銀行行員,收到一張支票時,會先核對簽名與金額才做任何事,並保留銀行自己的一份紀錄,而不是倚賴顧客那張事後可能被顧客塗改的單據。防禦性複製與引數驗證,就是這兩個習慣的程式設計版本:別信任進來的東西,也別倚賴別人能在你背後改動的資料。

驗證引數的意思是:函式在使用輸入之前,先把它們對照自己的前置條件檢查一遍——這個指標非 NULL 嗎、這個長度在界內嗎、這個索引在範圍內嗎、這個列舉值是我真的有處理的嗎?在 C 裡這件事格外重要,因為語言不會替你檢查——傳一個 NULL 指標或一個越界索引,你得到的是崩潰或默默損壞,而不是一個乾淨的錯誤。所以一個健壯的函式會在開頭檢視它的引數,並以清楚的錯誤回傳拒絕壞的引數,尤其在資料來自外部(使用者、檔案、網路)的信任邊界處。防禦性複製處理一個更微妙的危險:當一個函式拿到一個指向它稍後會倚賴的資料的指標,而呼叫者仍然也握著那個指標時,呼叫者(或另一條執行緒)可能在你眼皮底下改動或釋放那份資料。為你所倚賴的資料做一份你自己的私有副本,能讓你不受這種事後變動的影響——你推理的是一份由你掌控的快照,而非一個移動中的目標。經典例子是儲存呼叫者提供的字串:如果你只留指標,呼叫者可能覆寫或釋放那塊緩衝區;如果你用 strdup() 做自己的副本,你的資料就安全了(而現在你擁有那份副本,必須負責釋放它)。

為何重要,以及誠實的權衡:這些技巧把含糊、遙遠的崩潰,轉化成在門口當場抓到的、本地的、可解釋的錯誤。它們最有價值之處,正是在信任邊界——資料從不可信來源跨進你程式碼的那道接縫。要提醒的是成本與判斷:到處驗證、到處複製,會增加程式碼與執行期開銷,而複製又帶來一個新的所有權問題(誰來釋放這份副本?)。務實的規則是:在邊界處嚴格——把不可信輸入在那裡徹底、一次驗證好——並在更內部倚賴你自己已強制的不變式(以及對臭蟲用斷言),而不是在每個內部函式裡重複檢查同一件事。另外要注意它和斷言的區別:驗證不可信輸入是「可復原錯誤的處理」,必須永遠執行,而不是一個在 NDEBUG 下會消失的除錯專用斷言。

驗證:int parse(const char *s, size_t n) { if (s == NULL || n == 0) return -1; /* 在門口拒絕壞輸入 */ ... }。防禦性複製:struct config *make(const char *name) { struct config *c = malloc(sizeof *c); c->name = strdup(name); /* 我們自己的副本——呼叫者可能釋放他們的 */ return c; }——現在無論呼叫者對它的字串做什麼,c->name 都能存活。

在邊界拒絕壞引數;複製你稍後必須倚賴的資料。

驗證不可信輸入屬於可復原錯誤的處理,必須永遠執行——別用除錯專用的 assert() 來做,因為那在 NDEBUG 下會消失。而一份防禦性副本會製造一個新的擁有者:做副本的人必須釋放它,否則你只是把一個共享的臭蟲換成了一個洩漏。

又称
argument validationinput checking at the boundary防禦性程式設計