在有兩種情況下會調用析構函數。第一種是在正常情況下删除一個對象,例如對象超出了作用域或被顯式地delete。第二種是異常傳遞的堆棧輾轉開解(stack-unwinding)過程中,由異常處理系統删除一個對象。
在上述兩種情況下,調用析構函數時異常可能處于激活狀态也可能沒有處于激活狀态。遺憾的是沒有辦法在析構函數内部區分出這兩種情況。是以在寫析構函數時你必須保守地假設有異常被激活。因為如果在一個異常被激活的同時,析構函數也抛出異常,并導緻程式控制權轉移到析構函數外,C++将調用terminate函數。這個函數的作用正如其名字所表示的:它終止你程式的運作,而且是立即終止,甚至連局部對象都沒有被釋放。
下面舉一個例子,一個Session類用來跟蹤線上計算機的sessions,session就是運作在從你一登入計算機開始一直到登出出系統為止的這段期間的某種東西。每個Session對象關注的是它建立與釋放的日期和時間:
class Session {
public:
Session();
~Session();
...
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
};
函數logCreation 和 logDestruction被分别用于記錄對象的建立與釋放。我們是以可以這樣編寫Session的析構函數:
Session::~Session()
{
logDestruction(this);
}
一切看上去很好,但是如果logDestruction抛出一個異常,會發生什麼事呢?異常沒有被Session的析構函數捕獲住,是以它被傳遞到析構函數的調用者那裡。但是如果析構函數本身的調用就是源自于某些其它異常的抛出,那麼terminate函數将被自動調用,徹底終止你的程式。這不是你所希望發生的事情。程式沒有記錄下釋放對象的資訊,這是不幸的,甚至是一個大麻煩。那麼事态果真嚴重到了必須終止程式運作的地步了麼?如果沒有,你必須防止在logDestruction内抛出的異常傳遞到Session析構函數的外面。唯一的方法是用try和catch blocks。一種很自然的做法會這樣編寫函數:
Session::~Session()
{
try {
logDestruction(this);
}
catch (...) {
cerr << "Unable to log destruction of Session object "
<< "at address "
<< this
<< ".\n";
}
}
但是這樣做并不比你原來的代碼安全。如果在catch中調用operator<<時導緻一個異常被抛出,我們就又遇到了老問題,一個異常被轉遞到Session析構函數的外面。
我們可以在catch中放入try,但是這總得有一個限度,否則會陷入循環。是以我們在釋放Session時必須忽略掉所有它抛出的異常:
Session::~Session()
{
try {
logDestruction(this);
}
catch (...) { }
}
catch表面上好像沒有做任何事情,這是一個假象,實際上它阻止了任何從logDestruction抛出的異常被傳遞到session析構函數的外面。我們現在能高枕無憂了,無論session對象是不是在堆棧退棧(stack unwinding)中被釋放,terminate函數都不會被調用。
不允許異常傳遞到析構函數外面還有第二個原因。如果一個異常被析構函數抛出而沒有在函數内部捕獲住,那麼析構函數就不會完全運作(它會停在抛出異常的那個地方上)。如果析構函數不完全運作,它就無法完成希望它做的所有事情。例如,我們對session類做一個修改,在建立session時啟動一個資料庫事務(database transaction),終止session時結束這個事務:
Session::Session() // 為了簡單起見,,
{ // 這個構造函數沒有
// 處理異常
logCreation(this);
startTransaction(); // 啟動 database transaction
}
Session::~Session()
{
logDestruction(this);
endTransaction(); // 結束database transaction
}
如果在這裡logDestruction抛出一個異常,在session構造函數内啟動的transaction就沒有被終止。我們也許能夠通過重新調整session析構函數内的函數調用順序來消除問題,但是如果endTransaction也抛出一個異常,我們除了回到使用try和catch外,别無選擇。
綜上所述,我們知道禁止異常傳遞到析構函數外有兩個原因,第一能夠在異常轉遞的堆棧輾轉開解(stack-unwinding)的過程中,防止terminate被調用。第二它能幫助確定析構函數總能完成我們希望它做的所有事情。(如果你仍舊不很信服我所說的理由,可以去看Herb Sutter的文章Exception-Safe Generic Containers ,特别是“Destructors That Throw and Why They’re Evil”這段)。