C 異常使用須知

2021-09-27 13:47:07 字數 4634 閱讀 4488

本文翻譯自

相比錯誤**,異常為錯誤處理提供了很多便利。這些好處包括:

儘管有這些好處,然而大部分人仍然介懷異常的額外開銷而不願意使用。基於異常的實現機制,額外開銷來自兩方面:時間開銷(增加執行時間)和空間開銷(增加可執行檔案和記憶體消耗)。在這二者之中,時間開銷更被關注。然而,對於乙個良好的c++異常實現,除非真的有異常被丟擲,在正常執行時並不會引入執行時間開銷[2]。c++異常帶來的真正問題並不是執行效能,而是如何正確的使用異常。下面是一些對正確使用c++異常非常有用的事實。

考慮下面**的情況

try

catch(std::exception & ex)

如果myclass1:: dosomework()方法丟擲了異常,在**執行離開try **塊之前,因為obj1是乙個正常構造了的物件,obj1的析構函式需要被呼叫。那麼請試想一下,如果在myclass1的析構函式裡面也丟擲了異常會發生什麼?這個異常丟擲時,有乙個異常正處於active狀態。如果異常在丟擲時,有另外乙個異常處於active狀態,c++的行為是呼叫terminate()方法,這個方法的作用是終止當前應用程式。因此要想避免兩個異常同時處於active狀態,析構函式一定不能丟擲異常。

當乙個異常被丟擲時,鑑於原始的異常物件在堆疊回滾(stack unwinding)時會被析構掉,這個異常總是需要被重新拷貝乙份。因此這個物件的拷貝建構函式一定會被呼叫。如果我們程式設計時沒有寫拷貝建構函式,那麼c++會為我們提供乙個預設的拷貝建構函式。但是會出現一些情況,預設的拷貝建構函式無法正常工作;尤其時當類成員時指標的時候。使用這樣的物件作為異常物件時,一定要確保我們提供了正確的拷貝建構函式。現在,重點來了,c++裡面物件拷貝所使用的拷貝建構函式是基於靜態型別,而不是動態型別。考慮下面的**:

class widget ;

class specialwidget: public widget ;

void passandthrowwidget()

在上面的**中,throw語句丟擲了乙個widget型別的物件,rw的靜態型別是widget。這可能並不是我們想要的執行行為。

catch語句可以有三種方式來捕獲異常:

以值(by value)捕獲

以指標捕獲

以引用捕獲

以值捕獲成本高昂,且會遭遇到分片(slicing)問題。成本高昂是因為這種方式每次都需要建立兩個異常物件。因為堆疊回滾時,這個異常的原始物件可能因為超出作用域而被析構,因此當乙個異常丟擲時,無論這個異常是否**獲,都需要建立這個異常的乙個拷貝。如果這個異常是按值(以值)捕獲,就需要建立另外乙個拷貝以便傳給catch語句。因此,如果異常時按值捕獲,會有兩個異常物件被建立,導致異常處理過程變慢。

分片問題來自於這樣的場景,當乙個子類物件被throw丟擲,但catch語句的宣告卻是父類型別。在這種情況下,catch語句只會收到父類的拷貝,顯然這樣丟失了原始異常物件的屬性。因此實際使用中,一定要避免按值捕獲異常。

如果是按指標來捕獲異常,**會像下面這樣:

void dosomething()

catch (exception* ex)

}

為了以指標方式捕獲異常,丟擲時就應該以指標丟擲,而且丟擲異常的地方必須保證異常物件在堆疊回滾後仍然可用。儘管仍然會建立異常物件的副本,但這時建立的副本是指標。因而必須有其他手段來保證異常物件在丟擲後的可用。這點是可以做到的,可以把指標指向全域性或者靜態物件,或者把異常物件建立在堆裡。

然而,異常的捕獲者對於異常物件是如何建立的缺毫無主意,因此他也無法確定是否應該delete掉收到的異常物件指標。所以按指標捕獲異常是欠妥的做法。此外,所有從標準函式丟擲的異常都是物件,而不是指標。

按引用捕獲異常不會有上面』按指標』或者『按值』捕獲帶來的任何問題。使用者不需要擔心如何delete捕獲的異常物件。而且因為傳遞的是原始異常物件的引用,也不會有額外的異常物件被複製。

除此之外,按引用傳遞不會出現分片問題(slicing problem)。因此正確且高效的異常捕獲方法是按引用。

考慮下面的**

void somefunction()

在這個方法中,new操作建立了乙個******object物件,然後******object::dosomework()做了其他的工作,最終銷毀這個物件。但是如果object::dosomework()丟擲了異常,會發生什麼呢?在這個場景,我們沒有機會去deletepobj。這會導致記憶體洩漏。這只是乙個簡單的示例來展示異常可能導致的資源洩露,當然這個例子可以通過使用try catch語句來消除資源洩露。但是在實際條件下這種情況還是會在**的各個點發生而且很難被一眼發現。這種情況的乙個補救措施是使用標準庫的自動指標(std::auto_ptr)[1]。

考慮下面的示例**[4]:

template class stack

;template void stack::push(t element)

v[top] = element;

}

如果"out of memory"異常被丟擲,stack::push()方法會把stack物件留在乙個不一致狀態,因為stack的top已經被增加了,但是卻沒有push進去任何元素。當然,這個**可以修改避免著各種情況發生。在丟擲異常依然要特別留意,保證處於正確狀態的物件在丟擲異常後仍然是正確狀態。

進一步地,這種情況經常伴隨互斥量和鎖發生。在下面這個幼稚的threadsafequeue::pushback()方法實現中,如果dopushback()方法丟擲異常,_mutex將會保持被鎖的狀態,讓threadsafequeue物件處於不一致狀態。要克服這種場景,可以使用lock_guards,就像用自動指標避免記憶體洩漏一樣原理。需要注意的是lock_guard只在c++11後的標準庫里才有。然而你可以很容易實現乙個lock_guard類。

template void threadsafequeue::pushback(t element)

如果乙個方法丟擲了乙個其異常規約沒有列出的異常,這個錯誤會在執行時被檢測到,乙個特殊函式unexcepted()會被呼叫。這個函式的預設行為是呼叫terminate(),而terminate()的預設行為是呼叫abort()。所以乙個程式違反異常規約的預設行為後果就是終止執行。考慮下面的**:

void f1();                  // might throw anything

void f2() throw(int); //throws only int

void f2() throw(int)

當需要把不支援異常規約的舊**和新**一起整合時,這種場景下上面的**是合法的。但是如果f1()丟擲一些除int型別外的異常時程式就會終止執行,因為f2()不允許丟擲int外的型別。在這種情況想要恢復著實會有點難度,但仍然是有方法可以做到。參考1:第14條詳細介紹了這個問題的補救措施。

有兩種方法可以把乙個捕獲的異常傳遞給呼叫方,考慮下面的兩個**塊:

catch (widget& w)                 // catch widget exceptions

catch (widget& w)                 // catch widget exceptions

這兩個**塊的唯一區別就是第乙個丟擲當前異常,而第二個**塊丟擲了當前異常的乙個拷貝。第二種情況有2個問題。乙個是拷貝操作帶來的效能成本,另外就是分片(slicing)問題。如果異常物件是widget的子類,那麼異常物件只有widget部分被rethrow,這是因為拷貝操作是基於編譯時靜態型別進行的。

當異常被丟擲時,catch語句按照他們出現在**中的次序被匹配。在catch語句裡,乙個異常物件的型別也會匹配到他的父型別,因為子型別是父型別的子集。所以,當乙個子類物件被丟擲時,如果父類catch語句先出現在**裡,這個語句就會被執行。而不再管後面還有針對子型別的catch語句。

[1] more effective c++, by scott meyers, 1996.

[2] when and how to use exceptions, by herb sutter, 2004 (

[3] technical report on c++ performance, 2005 (

[4] exception handling: a false sense of security, by tom cargill (

C 異常處理須知

帖子內容 第一部分 1.異常發生時,異常物件會沿函式呼叫棧的反方向丟擲,這個過程常稱為棧展開 堆疊解退 2.在棧展開過程中,如果異常物件始終都沒遇到可行的 catch 處理塊,系統將呼叫 terminate 函式強制終止程式。當然如果連 try 塊都沒有,系統將直接呼叫 terminate 函式。3...

C 異常處理須知

第一部分 1.異常發生時,異常物件會沿函式呼叫棧的反方向丟擲,這個過程常稱為棧展開。2.在棧展開過程中,如果異常物件始終都沒遇到可行的catch處理塊,系統將呼叫terminate函式強制終止程式。當然如果連try塊都沒有,系統將直接呼叫terminate函式。3.在棧展開過程中,編譯器保證適當的撤...

C 異常處理須知

帖子內容 第一部分 1.異常發生時,異常物件會沿函式呼叫棧的反方向丟擲,這個過程常稱為棧展開 堆疊解退 2.在棧展開過程中,如果異常物件始終都沒遇到可行的 catch 處理塊,系統將呼叫 terminate 函式強制終止程式。當然如果連 try 塊都沒有,系統將直接呼叫 terminate 函式。3...