C與C++中的異常處理2(part1) (轉)

worldblog發表於2007-12-12
C與C++中的異常處理2(part1) (轉)[@more@]

對異常處理方法的擴充套件:namespace prefix = o ns = "urn:schemas-microsoft-com::office" />

  前次,我概述了異常的分類和C標準庫支援的處理方法。這次討論Microsoft對這些方法的擴充套件:結構化異常處理(SEH)和Microsoft Foundation Class (MFC)異常處理。SEH對C和C++都有效,MFC異常體系只對C++有效。

 

1.1  機構化異常處理

  機構化異常處理是提供的服務功能並對所有語言寫的有效。在Visual C++中,Microsoft封裝和簡化了這些服務(透過非標準的關鍵字和庫程式)。Windows平臺的其它可能選擇不同的方式來到達相似的結果。在這個專欄中,名詞“Structured Exception Handling”和“SEH”專指Visual C++對Windows異常服務的封裝。

 

1.2  關鍵字

  為了支援SEH,Micorsoft用四個新關鍵字擴充套件了C和C++語言:

l  __except

l  __finally

l  __leave

l  __try

  因為這是非標關鍵字,必須開啟擴充套件選項後再編譯(關掉/Fa)。

  為什麼這些關鍵字帶下劃線?C++標準(條款17.4.3.1.2,“Global names”)規定:

  下列名字和總是保留給編譯器:

l  所有帶雙下劃線(__)或以一個下劃線加一個大寫字母開始的名字保留給編譯器隨意使用。

l  所有以一個下劃線開始的名字保留給編譯器作全域性名稱用。

  C標準有類似的申明。

  既然SEH的關鍵字元合上面的規則,Microsoft就有權這樣使用它們。這也表明,你不被允許在自己的程式中使用保留的名字。你必須避免定義名字類似__MYHEADER_H__或_alError的識別符號。

  有趣而又不幸地,Visual C++的application wizards產生的使用了保留的識別符號。例如,如果你用ATL App Wizard生成一個新的service,結果程式碼定義瞭如_Handler和_twinMain的名字--標準所說的你的程式不能使用的保留名稱。

  要減少這個不合規定行為,你當然可以手工更改這些名稱。還好,這些有疑問的名字都是類的私有變數,在類的定義外面是不可見的,在.h和.cpp中進行全域性替換是可行的。不幸的是,有一個函式(_twinMain)和一個(_Module)被申明瞭extern,也就是說程式的其它部分會假定你使用了這些名字。(事實上,Visual C++庫libc.lib在連線時需要名字_twinMain可用。)

  我建議你保留Wizard生成的名字,不要在你自己的程式碼中定義這樣的名字就可以了。另外,你應該將所有不合標準的定義寫入文件並留給程式的維護人員;記住,Visual C++以後的版本(和現有的其它C++編譯器)可能以另外的方式使用這些名字,從而破壞了你的程式碼。

1.3  識別符號

  Microsoft也在非標頭excpt.h中定義了幾個SEH的識別符號,並且包含入windows.h中。在其內部,定義了:

l  供__except的過濾使用的過濾結果宏。

l  物件和函式的別名宏,用於查詢異常資訊和狀態。

l  偽關鍵字宏,和前面談到的四個關鍵字有著相同名字和含義,但沒有下劃線。(例如,宏leave對應SEH關鍵字__leave。)

  Microsoft用這些宏令我抓狂。他們對同一個函式了定義多個別名。例如,excpt.h有如下申明和定義:

unsigned long __cdecl  _exception_code(void);

#define GetExceptionCode _exception_code

#define exception_code  _exception_code

 

  也就是說,你可以用三種方法同一函式。你用哪個?並且,這些別名會如你所期望地被維護嗎?

  在Microsoft的文件中,它看起來偏愛GetExceptionCode,它的名字和其它全域性Windows 函式風格一致。我在MSDN中搜尋到33處GetExceptionCode,兩個_exception_code,而exception_code個數為0。根據Microsoft的引導,推薦使用GetExceptionCode及類似名稱的其它函式。

  因為_exception_code的兩個別名是宏,所以你不能再使用同樣的名字了。我曾經犯過這個錯,當我在為這個專欄寫例程的時候。我定義了一個區域性物件叫exception_code(大概是吧)。實際上我就是定義了一個區域性物件叫_exception_code,這是我無意中使用的宏exception_code展開的結果。當我一想到是這個問題,解決方案就是簡單地將我的物件名字從exception_code改為code。

  最後,excpt.h定義了一個特別的宏--“try”--已經成為C++真正的關鍵字的東西。這意味著你不能在包含了excpt.h的編譯單元中簡單地混合SEH和標準C++的異常塊,除非你願意#undef這個try宏。當這樣undef而露出真正的try關鍵字時,要冒搞亂SEH的維護人員大腦的危險。另一方面,精通標準C++的程式設計師會將try理解為一個關鍵字而不是宏。

  我認為,包含一個標頭檔案(即使是象excpt.h這樣的非標標頭檔案)不應該改變符合語言標準的程式碼的行為。我更堅持掩蓋或重定義掉語言標準定義的關鍵字是個壞習慣。我建議:#undef try,同樣不使用其它的偽關鍵字宏,直接使用真正的關鍵字(如__try)。

 

1.4  語法

  最基本的SEH語法是try塊。如下形式:

__try compound-statement handler

 

處理體:

__except ( filter-expression ) compound-statement

 

或:

__finally compound-statement

 

完整一點看,try塊如下:

__try

  {

  ...

  }

__except(filter-expression)

  {

  ...

  }

 

 

或:

__try

  {

  ...

  }

__finally

  {

  ...

  }

 

在__try裡面你必須使用一個leave語句:

__try

  {

  ...

  __leave;

  ...

  }

 

在更大的程式塊中,一個try塊被認為是個單條語句:

if (x)

  {

  __try

  {

  ...

  }

  __finally

  {

   ...

  }

  } 

 

等價於:

if (x)

  __try

  {

  ...

  }

  __finally

  {

  ...

  }

 

  其它注意點:

l  在給定的try塊中你必須有一個正確的異常處理函式。

l  所有的語句必須合併。即使只有一條語句跟在__try、__except或__finally後面也必須將它放入{}中。

l  在異常處理函式中,相應的過濾表示式必須有一個或能轉換為一個int型的值。

1.5  基本語意

  上次我列舉了異常生命期的5個階段。在SEH體系下,這些階段實現如下:

l  操作上報了一個錯誤或檢測到了一個錯誤,或程式碼檢測到一個錯誤(階段1)。

l  (通常是由使用者呼叫Win32函式RasieException啟動,)產生並觸發一個異常物件(階段2)。這個物件是一個結構,其屬性對異常處理函式可見。

l  異常處理函式“看到”異常,並且有機會捕獲它(階段3和4)。取決於處理函式的意願,異常將或者恢復或者終止。(階段5)。

  一個簡單的例子:

int filter(void)

  {

  /* Stage 4 */

  }

int main(void)

  {

  __try

  {

  if (some_error) /* Stage 1 */

  RaiseException(...); /* Stage 2 */

  /* Stage 5 of resuming exception */

  }

  __except(filter()) /* Stage 3 */

  {

  /* Stage 5 of tenating exception */

  }

  return 0;

  }

 

  Microsoft呼叫定義在__except中的異常處理函式,和定義在__finally中的終止函式。

  一旦異常被觸發,由__except開始的異常處理函式被異常發生點順函式呼叫鏈向外面詢問。每個被發現的異常處理函式,其過濾表示式都被求值。每次求值後發生什麼取決於其返回結果。

  excpt.h定義了3個過濾結果的宏,都是int型的:

l  EXCEPTION_CONTINUE_EXECUTION = -1

l  EXCEPTION_CONTINUE_SEARCH = 0

l  EXCEPTION_EXECUTE_HANDLER = 1

  前面我說過,過濾表示式必須相容int型,所以它們和這3個宏的值匹配。這個說法太保守了:我的顯示Visual C++接受的過濾表示式可以具有所有的整型、指標型、結構、陣列甚至是void型!(但我在嘗試浮點指標時遇到了編譯錯誤。)

  更進一步,所有求出的值看來都有效(至少對整型如此)。所有非零且符號位為0的值效果相當於EXCEPTION_EXECUTE_HANDLER,而符號位為1的相當於EXCEPTION_CONTINUE_EXECUTION。這大概是按位取模的結果。

  如果一個異常處理函式的過濾求值結果是EXCEPTION_CONTINUE_SEARCH,這個處理函式拒絕捕獲異常,將繼續搜尋下一個異常處理函式。

  透過由過濾表示式產生一個非EXCEPTION_CONTINUE_SEARCH來捕獲異常,一旦捕獲,程式就恢復。怎麼恢復仍然由過濾表示式的值決定:

l  EXCEPTION_CONTINUE_EXECUTION:表現為恢復異常。從發生異常處下面開始。異常處理函式本身的程式碼不執行。

l  EXCEPTION_EXECUTE_HANDLER:表現為終止異常。從異常發生處開始退棧,一路上所遇到終止函式都被執行。棧退到捕獲異常的處理函式所在的一級為止。進入處理函式體並執行。

  如名所示,終止處理函式(以__finally開始的程式碼)在終止異常時被呼叫。裡面是clean up程式碼,它們就象C標準庫中的atexit()函式和C++的解構函式。終止處理函式在正常執行流程也會進入,就象不是捕獲型程式碼。相反,異常處理函式總表現為捕獲型:它們只在其過濾表示式求值為EXCEPTION_EXECUTE_HANDLER時才進入。

  終止處理函式並不明確知道自己是從正常流程進入的還是在一個try塊異常終止時進入的。要判斷這點,可以呼叫AbnormalTermination函式。此函式返回一個int,0表明是從正常流程進入的,其它值表明在異常終止時進入的。

  AbnormalTermination實際上是個指向_abnormal_termination()的宏。Visual C++將_abnormal_termination()設計為環境敏感的函式,就象一個關鍵字。你不能隨便呼叫這個函式,只能在終止處理函式中呼叫。這意味著你不能在終止處理函式中呼叫一箇中間函式,再在此中間函式中呼叫_abnormal_termination(),這樣做會得到一個編譯期錯誤。

 

1.6  例程

  下面的C例子顯示了不同的過濾表示式值和處理函式本身型別的相互作用。第一個版本是個小的完整程式,以後的版本都在它前面一個上有小小的改動。所有的版本都自解釋的,你能看清流程和行為。

  程式透過RaiseException()觸發一個異常物件。RaiseException()函式的第一個引數是異常的程式碼,型別是32位無符號整型(D);Microsoft為使用者自定義的錯誤保留了[0xE0000000,0xEFFFFFFF]的範圍。其它引數一般填0。

  這裡使用的異常過濾器很簡單。實際使用中,大概要呼叫GetExceptionCode()和GetExceptionInformation()來查詢異常物件的屬性。

1.7  Version #1: Terminating Exception

  用Visual C++生成一個空的Win32控制檯程式,命名為SEH_test,選項為預設。將下列C原始碼加入工程檔案:

#include

#include "windows.h"

#define filter(level, status)

  (

  printf("%s:%*ilter => %sn",

  #level, (int) (2 * (level)), "", #status),

  (status)

  )

#define termination_trace(level)

  printf("%s:%*shandling %snormal terminationn",

  #level, (int) (2 * (level)), "",

  AbnormalTermination() ? "ab" : "")

static void trace(int level, char const *message)

  {

  printf("%d:%*s%sn", level, 2 * level, "", message);

  }

extern int main(void)

  {

  DWORD const code = 0xE0000001;

  trace(0, "before first try");

  __try

  {

  trace(1, "try");

  __try

  {

  trace(2, "try");

  __try

  {

  trace(3, "try");

  __try

  {

  trace(4, "try");

  trace(4, "raising exception");

  RaiseException(code, 0, 0, 0);

  trace(4, "after exception");

  }

  __finally

  {

  termination_trace(4);

  }

  end_4:

  trace(3, "continuation");

  }

  __except(filter(3, EXCEPTION_CONTINUE_SEARCH))

  {

  trace(3, "handling exception");

  }

  trace(2, "continuation");

  }

  __finally

  {

  termination_trace(2);

  }

  trace(1, "continuation");

  }

  __except(filter(1, EXCEPTION_EXECUTE_HANDLER))

  {

  trace(1, "handling exception");

  }

  trace(0, "continuation");

  return 0;

  }

 

  現在編譯程式碼。(可能會得到label end_4未用的警告;先忽略。)

  注意:

l  程式有四個巢狀try塊,兩個有異常處理函式,兩個有終止處理函式。為了更好地顯示巢狀和控制流程,我把它們全部放入同一個函式中。實際中可能是放在多個函式或多個編譯單元中的。

l  追蹤執行情況,輸出結果顯示當前塊的巢狀層次。

l  異常過濾器被實現為宏。第一個引數是巢狀層次,第二個才是實際要處理的值。

l  終止處理函式透過termination_trace宏跟蹤其執行情況,顯示出呼叫它們的原因。(記住,終止處理函式即使沒有發生異常也會進入的。)

  執行此程式,將看到如下輸出:

0:before first try

1:  try

2:  try

3:  try

4:  try

4:  raising exception

3:  filter => EXCEPTION_CONTINUE_SEARCH

1:  filter => EXCEPTION_EXECUTE_HANDLER

4:  handling abnormal termination2:  handling abnormal termination

1:  handling exception

0:continuation

 

  事件鏈:

l  第四層try塊觸發了一個異常。這導致順巢狀鏈向上搜尋,查詢願意捕獲這個異常的異常過濾器。

l  碰到的第一個異常過濾器(在第三層)得出了EXCEPTION_CONTINUE_SEARCH,所以拒絕捕獲這個異常。繼續搜尋下一個異常處理函式。

l  碰到的下一個異常過濾器(在第一層)得出了EXCEPTION_EXECUTE_HANDLER。這次,這個過濾器捕獲這個異常。因為它求得的值,異常將被終止。

l  控制權回到異常發生點,開始退棧。沿路所有的終止處理函式被執行,並且所有的處理函式都知道異常終止發生了。一直退棧到控制權回到捕獲異常的異常處理函式(在第一層)。在退棧時,只有終止處理函式被執行,中間的其它程式碼被忽略。

l  控制權一回到捕獲異常的異常處理函式(在第一層),將以正常狀態繼續執行。

  注意,控制權在同一巢狀層傳遞了兩次:第一次異常過濾表示式求值,第二次在退棧和執行終止處理函式時。這造成了一種危害可能:如果一個異常過濾表示式以某種終止處理函式不期望的方式修改了的什麼。一個基本原則就是,你的異常過濾器不能有副作用;如果有,則必須為你的終止處理函式儲存它們。

 

1.8  版本2:未捕獲異常

  將例程中的這行:

__except(filter(1, EXCEPTION_EXECUTE_HANDLER))

改為

__except(filter(1, EXCEPTION_CONTINUE_SEARCH))

 

  於是沒有異常過濾器捕獲這個異常。執行修改後的程式,你將看到:

0:before first try

1:  try

2:  try

3:  try

4:  try

4:  raising exception

3:  filter => EXCEPTION_CONTINUE_SEARCH

1:  filter => EXCEPTION_CONTINUE_SEARCH

 

接著出現這個對話方塊:

 

1. 使用者異常對話方塊

點“Details”將其展開

 

2. 使用者異常對話方塊的詳細資訊

在出錯資訊中可看到:出錯程式是SEH_TEST,透過RaiseException丟擲的原始異常碼是e0000001H。

這個異常漏出了程式,最後被作業系統捕獲和處理。有些象你的程式是這麼寫的:

__try

  {

  int main(void)

  {

  ...

  }

  }

__except(exception_dialog(), EXCEPTION_EXECUTE_HANDLER)

  {

  }

 

按對話方塊上的“Close”,所有的終止處理函式被執行,並退棧,直到控制權回到捕獲異常的處理函式。你可以明顯看到這些資訊:

4:  handling abnormal termination

2:  handling abnormal termination

  它們出現在關閉對話方塊之後。注意,你沒有看到:

0:continuation

  因為它的實現程式碼在終止處理函式之外,而退棧時只有終止處理函式被執行。

  對我們的試驗程式而言,捕獲異常的處理函式在main之外,這意味著傳遞異常的行為到了程式範圍外仍然在繼續。其結果是,程式被終止了。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-992207/,如需轉載,請註明出處,否則將追究法律責任。

相關文章