程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++異常和錯誤處理經驗談

C++異常和錯誤處理經驗談

編輯:關於C++

代替 try / catch / throw 的通常做法是返回一個返回代碼(有時稱為錯誤代碼),例如,printf(), scanf() 和 malloc()就是這樣工作的:調用者通過if等語句來測試返回值判斷函數是否成功。

盡管返回代碼技術有時是最適當的錯誤處理技術,但會增加不必要的if語句這樣的令人討厭的效果。

質量降級:眾所周知,條件語句可能包含的錯誤大約十倍於其他類型的語句。因此,在其他都相同時,如果你能從代碼中消除條件語句,你會得到更健壯的代碼。

推遲面市:由於條件語句是分支點,而它們關系到白盒法測試時的測試條件的個數,因此不必要的條件語句會增加測試的時間總量。如果你沒有走過每個分支點,那麼你的代碼中就會有在測試中沒有被執行過的指令,直到用戶/客戶發現它,那就糟糕了。

增加開發成本:不必要的控制流程的復雜性增加了尋找bug,修復bug,和測試的工作。

因此,相對於通過返回代碼和if來報告錯誤,使用try / catch / throw所產生更少有bug,更低的開發成本和更快面市的代碼。

如何處理構造函數的失敗?

構造函數沒有返回類型,所以返回錯誤代碼是不可能的。因此拋出異常是標記構造函數失敗的最好方法。

如果你沒有或者不願意使用異常,這裡有一種方法。如果構造函數失敗了,構造函數可以把對象帶入一種“僵屍”狀態。你可以通過設置一個內部狀態位使對象就象死了一樣,即使從技術上來說,它仍然活著。然後加入一個查詢(“檢察員”)成員函數,以便類的用戶能夠通過檢查這個“僵屍位”來確定對象是真的活著還是已經成為僵屍(也就是一個“活著的死對象”)。你也許想有另一個成員函數來檢查這個僵屍位,並且當對象並不是真正活著的時候,執行一個 no-op(或者是更令人討厭的如 abort())。這樣做真的不漂亮,但是如果你不能(或者不想)使用異常的話,這是最好的方法了。

如何處理析構函數的失敗?

往log文件中寫一個消息。但不要拋出異常!

C++的規則是你絕對不可以在另一個異常的被稱為“棧展開(stack unwinding)”的過程中時,從析構函數拋出異常。舉例來說,如果某人寫了throw Foo(),棧會被展開,以至throw Foo()和 } catch (Foo e) { 之間的所有的棧頁面被彈出。這被稱為棧展開(statck unwinding)

在棧展開時,棧頁面中的所有的局部對象會被析構。如果那些析構函數之一拋出異常(假定它拋出一個Bar對象),C++運行時系統會處於無法決斷的境遇:應該忽略Bar並且在} catch (Foo e) { 結束?應該忽略Foo並且尋找 } catch (Bar e) { ?沒有好的答案——每個選擇都會丟失信息。

因此C++語言擔保,當處於這一點時,會調用terminate()來殺死進程。突然死亡。

防止這種情況的簡單方法是不要從析構函數中拋出異常。但如果你真的要聰明一點,你可以說"當處理另一個異常的過程中時,不要從析構函數拋出異常"。但在第二種情況中,你處於困難的境地:析構函數本身既需要代碼處理拋出異常,還需要處理一些“其他東西”,調用者沒有當析構函數檢測到錯誤時會發生什麼的擔保(可能拋出異常,也可能做一些“其他事情”)。因此完整的解決方案非常難寫。因此索性就做一些“其他事情”。也就是,不要從析構函數中拋出異常。

當然,由於總有一些該規則無效的境況,這些話不應該被“引證”。但至少99%的情況下,這是一個好規則。

如果構造函數會拋出異常,我該怎樣處理資源?

對象中的每個數據成員應該清理自己。

如果構造函數拋出異常,對象的析構函數將不會運行。如果你的對象需要撤銷一些已經做了的動作(如分配了內存,打開了一個文件,或者鎖定了某個信號量),這些需要被撤銷的動作必須被對象內部的一個數據成員記住。

例如,應該將分配的內存賦給對象的一個“智能指針”成員對象Fred,而不是分配內存給未被初始化的Fred* 數據成員。這樣當該智能指針消亡時,智能指針的析構函數將會刪除Fred對象。標准類auto_ptr就是這種“智能指針”類的一個例子。你也可以寫你自己的引用計數智能指針。

當別人拋出異常時,我如何改變字符數組的字符串長度來防止內存洩漏?

如果你要做的確實需要字符串,那麼不要使用char數組,因為數組會帶來麻煩。應該用一些類似字符串類的對象來代替。

例如,假設你要得到一個字符串的拷貝,隨意修改這個拷貝,然後在修改過的拷貝的字符串末尾添加其它的字符串。字符數組方法將是這樣:

void userCode(const char* s1, const char* s2)
{
// 制作s1的拷貝:
char* copy = new char[strlen(s1) + 1];
strcpy(copy, s1);
// 現在我們有了一個指向分配了的自由存儲的內存的指針,
// 我們需要用一個try塊來防止內存洩漏:
try {
// ... 現在我們隨意亂動這份拷貝...
// 將s2 添加到被修改過的 copy 末尾:
// ... [在此處重分配 copy] ...
char* copy2 = new char[strlen(copy) + strlen(s2) + 1];
strcpy(copy2, copy);
strcpy(copy2 + strlen(copy), s2);
delete[] copy;
copy = copy2;
// ... 最後我們再次隨意亂動拷貝...
} catch (...) {
delete[] copy; // 得到一個異常時,防止內存洩漏
throw; // 重新拋出當前的異常
}
delete[] copy; // 沒有得到異常時,防止內存洩漏
}

象這樣使用char*s是單調的並且容易發生錯誤。為什麼不使用一個字符串類的對象呢?你的編譯器也許提供了一個字符串類,而且它可能比你自己寫的char*s更快,當然也更簡單、更安全。例如,如果你使用了標准化委員會的字符串類std::string,你的代碼看上去就會象這樣:

#include <string> // 讓編譯器找到 std::string 類

void userCode(const std::string& s1, const std::string& s2)
{
std::string copy = s1; // 制作s1的拷貝
// ... 現在我們隨意亂動這份拷貝...
copy += s2; // A將 s2 添加到被修改過的拷貝末尾
// ... 最後我們再次隨意亂動拷貝...
}

函數體中總共只有兩行代碼,而前一個例子中有12行代碼。節省來自內存管理,但也有一些是來自於我們不必的調用strxxx()例程。這裡有一些重點:

由於std::string自動處理了內存管理,當增長字符串時,我們不需要先式地寫任何分配內存的代碼。

由於std::string自動處理了內存管理,在結束時不需要 delete[] 任何東西。

由於std::string自動處理了內存管理,在第二個例子中不需要 try 塊,即使某人會在某處拋出異常。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved