資源管理與清理問題(resource management and the cleanup problem)
想像露營。每搭一頂帳篷,事後都得收起來;每生一堆火,都得撲滅;每開一個罐頭,都得帶走。程式也一樣:它取得的幾乎每一項資源,事後都必須釋放。malloc() 來的記憶體必須用 free() 歸還;用 open() 開啟的檔案必須用 close() 關閉;用 pthread_mutex_lock() 取得的鎖必須用 pthread_mutex_unlock() 釋放。資源管理就是這樣一門紀律:在程式可能走的每一條路徑上,把每一次取得都恰好配上一次對應的釋放。
清理問題正是讓這件事真正棘手的地方:真實程式碼裡,許多資源是依序取得的,而中間任一步都可能失敗。假設某函式先開檔、再配置緩衝區、再取鎖、然後做事。如果取鎖失敗,你必須在回傳前釋放緩衝區並關檔——但不可解鎖,因為你根本沒鎖。如果緩衝區配置失敗,你必須關檔,但不可釋放緩衝區(根本沒有)也不可解鎖。每一個提早退出的點,都得「恰好」回復到目前為止取得的資源,不多也不少。少清就洩漏(記憶體洩漏、檔案描述符洩漏、鎖沒放);多清就重複釋放、或解開一個你從沒鎖過的東西,那是未定義行為。
為何重要:這是 C 的核心難處之一,也是為什麼會有那麼多技巧來馴服它。洩漏不總是無害的——一個長時間執行、每個請求都洩漏一個檔案描述符的伺服器,最終會撞到上限,再也無法接受新連線;一個洩漏的鎖則能讓其他每條執行緒永遠凍結(死結)。你會反覆遇到的那些模式——C 的 goto-cleanup 風格,以及 C++/Rust 的 RAII——都是對清理問題的直接回應,各自試圖保證釋放會在每一條退出路徑上發生,而不必程式設計師在每個 return 處親手寫出來。
一個會開檔、配置緩衝區、再鎖住互斥鎖的函式,有四個可能的退出點(每次取得各一個,加上正常結尾)。正常退出時它必須解鎖、釋放、關檔。如果取鎖失敗,它必須釋放並關檔,但「不可」解鎖。如果 malloc 失敗,它必須關檔,但「不可」釋放、「不可」解鎖。所需的清理,取決於你走到了多遠。
每個退出點都必須恰好回復到目前為止取得的東西——這正是清理問題的核心。
free() 和 close() 會釋放資源,卻不會清掉你的變數:free() 之後的指標在你把它設為 NULL 之前仍是懸置指標,而一個關閉了的描述符編號可能被下一次 open() 重用。清理和「讓變數變得可以安全再次碰觸」不是同一回事。