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

來源:互聯網
上載者:User

try / catch / throw 通過哪些方法來改善軟體品質?

代替 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 塊,即使某人會在某處拋出異常。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.