C++ 構造/解構函式中的異常處理
C++ 為什麼會引入(需要)異常?
The C++ Programming Language: 一個庫的作者可以檢測出發生了執行時錯誤,但一般不知道怎樣去處理它們(因為和使用者具體的應用有關);另一方面,庫的使用者知道怎樣處理這些錯誤,但卻無法檢查它們何時發生(如果能檢測,就可以再使用者的程式碼裡處理了,不用留給庫去發現)。
C++ primer: Exceptions let us separate problem detection from problem resolution(錯誤檢測和錯誤處理分離開).
建構函式中的異常
C++ 的建構函式沒有返回值,使用異常來處理建構函式中的錯誤(或者其它)是一種很好的辦法。但是一定在建構函式中使用異常一定要小心。
我們知道,當出現異常的時候,會呼叫類解構函式。然而,在建構函式中丟擲異常的時候,不會去呼叫解構函式,此時如果處理不當,會出現記憶體洩露。
如下:
class TestA { public: TestA() { std::cout << "TestA Contructor" << std::endl; } ~TestA() { std::cout << "TestA Destructor" << std::endl; } }; class TestB { public: TestB() { std::cout << "TestB Constructor" << std::endl; } ~TestB() { std::cout << "TestB Destructor" << std::endl; } }; class TestC { public: TestC() { ta = new TestA(); tb = new TestB(); throw std::string("something trigger a exception"); std::cout << "TestC() Constructor" << std::endl; } ~TestC() { delete ta; delete tb; std::cout << "TestC() Destructor" << std::endl; } private: TestA* ta; TestB* tb; }; int main() { try { TestC tc; } catch (const std::string& exp) { std::cout << exp << std::endl; } }
輸出:
TestA Contructor
TestB Constructor
something trigger a exception
ta 和 tb 記憶體洩露。如何避免這種問題呢?
class TestC { public: TestC() { try { ta = new TestA(); tb = new TestB(); throw std::string("something trigger a exception"); } catch(const std::string& exp) { std::cout << exp << std::endl; cleanup(); throw; } std::cout << "TestC() Constructor" << std::endl; } ~TestC() { cleanup(); std::cout << "TestC() Destructor" << std::endl; } void cleanup() { delete ta; ta = NULL; delete tb; tb = NULL; } private: TestA* ta; TestB* tb; }; int main() { try { TestC tc; } catch (...) { std::cout << "construtor failure." << std::endl; } }
輸出:
TestA Contructor
TestB Constructor
something trigger a exception
TestA Destructor
TestB Destructor
construtor failure.
新添加了一個 cleanup
函式,用來清理該類在堆上的資源。這麼做的好處:
- 當建構函式中基於某種原因丟擲異常時,手動把資源釋放,避免記憶體洩露。
- 丟擲一個空的異常,通知外圍的程式,TestC構造失敗了。
解構函式中的異常
解構函式的作用是釋放資源,如果某一行程式碼丟擲了異常,後面的程式碼將得不到執行,造成記憶體洩露。
詳細可以去看 Effective C++ item 08: Prevent exceptions from leaving destructors.
總結
看似這個問題簡單,很容易得到解決。然而,實際開發中面臨的情況會比上面複雜(惡劣)的多,比如 10 個指標,只完成了 3 個指標的初始化,某個指標的一個操作引發了異常。即便我們有 cleanup() 函式,因為其他指標沒有得到任何初始化(隨機值),在 delete 的時候一樣會程式崩潰。
我相信沒有一個解決此類的問題的通用方案。但是,我們可以用一些原則來避免出現問題:
- 建構函式/解構函式應該保持簡單,只完成成員的初始化和釋放資源,不要夾雜其它無關操作。
- 建構函式/解構函式應該在內部處理掉異常,不要依靠外圍程式來處理異常。
- 避免在建構函式中丟擲異常,禁止在解構函式中丟擲異常。
- 如果可以的話,可以把複雜的操作封裝成函式,在構造/解構函式中直接呼叫。比如建構函式僅僅完成基本的初始化(指標賦空等),用init()來做實際的初始化,用cleanup()做資源釋放,解構函式只調用。
其實核心思想就是保持程式邏輯的簡單即可,如果你的設計足夠合理,那麼就不會面臨這種問題。