http://www./dev/vc/2009-05-24/a09110607.shtml Visual C++提供了對(duì)C語(yǔ)言、C++語(yǔ)言及MFC的支持,因而其涉及到的異常(exception)處理也包含了這三種類型,即C語(yǔ)言、C++語(yǔ)言和MFC的異 常處理。除此之外,微軟對(duì)C和C++的異常處理進(jìn)行了擴(kuò)展,提出了結(jié)構(gòu)化異常處理(SEH)的概念,它支持C和C++(與之相比,MFC異常處理僅支持 C++)。 一個(gè)典型的異常處理包含如下幾個(gè)步驟: ?。?)程序執(zhí)行時(shí)發(fā)生錯(cuò)誤; (2)以一個(gè) 異常對(duì)象(最簡(jiǎn)單的是一個(gè)整數(shù))記錄錯(cuò)誤的原因及相關(guān)信息; (3)程序檢測(cè)到這個(gè)錯(cuò)誤(讀取異常對(duì)象); ?。?) 程序決定如何處理錯(cuò)誤; ?。?)進(jìn)行錯(cuò)誤處理,并在此后恢復(fù)/終止程序的執(zhí)行。 C、C++、MFC及SEH在這幾 個(gè)步驟中表現(xiàn)出了不同的特點(diǎn)。本文將對(duì)這四種異常處理進(jìn)行介紹,并對(duì)它們進(jìn)行對(duì)比分析。本文例程的調(diào)試平臺(tái)為Visual C++6.0,操作系統(tǒng)為Windows XP,所有程序均調(diào)試通過(guò)。
在進(jìn)入正式的講解之前,先說(shuō)
幾句廢話。許多的編程新手對(duì)異常處理視而不見(jiàn),程序里很少考慮異常情況。一部分人甚至根本就不考慮,以為程序總是能以正確的途徑運(yùn)行。譬如我們有的程序設(shè)
計(jì)者調(diào)用fopen打開(kāi)一個(gè)文件后,立馬就開(kāi)始進(jìn)行讀寫操作,根本就不考慮文件是否正常打開(kāi)了。這種習(xí)慣一定要改掉,縱使你再不愿意!這是軟件健壯性的需
要!異常處理不是浪費(fèi)時(shí)間! 1.C語(yǔ)言異常處理 1.1 異常終止 標(biāo)準(zhǔn)C庫(kù)提 供了abort()和exit()兩個(gè)函數(shù),它們可以強(qiáng)行終止程序的運(yùn)行,其聲明處于<stdlib.h>頭文件中。這兩個(gè)函數(shù)本身不能檢測(cè) 異常,但在C程序發(fā)生異常后經(jīng)常使用這兩個(gè)函數(shù)進(jìn)行程序終止。下面的這個(gè)例子描述了exit()的行為:
在這個(gè)例子中,main函數(shù)一開(kāi)始就執(zhí)行了exit函數(shù) (此函數(shù)原型為void exit(int)),因此,程序不會(huì)輸出"程序不會(huì)執(zhí)行到這里"。程序中的exit(EXIT_SUCCESS)表示程序正常結(jié)束,與之對(duì)應(yīng)的 exit(EXIT_FAILURE)表示程序執(zhí)行錯(cuò)誤,只能強(qiáng)行終止。EXIT_SUCCESS、EXIT_FAILURE分別定義為0和1。 對(duì)于exit函數(shù),我們可以利用atexit函數(shù)為exit事件"掛接"另外的函數(shù),這種"掛接"有點(diǎn)類似Windows編程中的"鉤子" (Hook)。譬如:
程序輸出"atexit掛接的函數(shù)"后即終止。來(lái)看下面的程序,我們不調(diào)用exit函數(shù),看看atexit掛接的函數(shù)會(huì)否執(zhí)行:
程序輸出: 不調(diào)用exit函數(shù) atexit掛接的函數(shù) 這說(shuō)明,即便是我們不調(diào)用exit 函數(shù),當(dāng)程序本身退出時(shí),atexit掛接的函數(shù)仍然會(huì)被執(zhí)行。 atexit可以被多次執(zhí)行,并掛接多個(gè)函數(shù),這些函數(shù)的執(zhí)行順序?yàn)? 后掛接的先執(zhí)行,例如:
輸 出的結(jié)果是: atexit掛接的函數(shù)3 atexit掛接的函數(shù)2 atexit掛接的函數(shù)1 在Visual C++中,如果以abort函數(shù)(此函數(shù)不帶參數(shù),原型為void abort(void))終止程序,則會(huì)在debug模式運(yùn)行時(shí)彈出如圖1所示的對(duì)話框。
1.2 斷言(assert) assert宏在C語(yǔ)言程序的調(diào)試中發(fā)揮著重要的作用,它用于檢測(cè)不會(huì)發(fā)生的情況,表明一旦發(fā)生了這樣的情況,程序就實(shí)際上執(zhí)行錯(cuò)誤了,例如 strcpy函數(shù):
其中包含斷言assert( (strDest != NULL) && (strSrc != NULL) ),它的意思是源和目的字符串的地址都不能為空,一旦為空,程序?qū)嶋H上就執(zhí)行錯(cuò)誤了,會(huì)引發(fā)一個(gè)abort。 assert宏的定義 為:
如果程序不在debug模式下,assert宏實(shí)際上什么 都不做;而在debug模式下,實(shí)際上是對(duì)_assert()函數(shù)的調(diào)用,此函數(shù)將輸出發(fā)生錯(cuò)誤的文件名、代碼行、條件表達(dá)式。例如下列程序:
在 此程序中,為了避免我們的strcpy與C庫(kù)中的strcpy重名,將其改為myStrcpy。程序的輸出如圖2:
失敗的斷言也會(huì)彈出如圖1所示的對(duì)話框,這是因?yàn)? _assert()函數(shù)中也調(diào)用了abort()函數(shù)。 一定要記住的是assert本質(zhì)上是一個(gè)宏,而不是一個(gè)函數(shù),因而不能把帶有 副作用的表達(dá)式放入assert的"參數(shù)"中。 1.3 errno errno在C程序中是一個(gè)全局變量,這個(gè)變 量由C運(yùn)行時(shí)庫(kù)函數(shù)設(shè)置,用戶程序需要在程序發(fā)生異常時(shí)檢測(cè)之。C運(yùn)行庫(kù)中主要在math.h和stdio.h頭文件聲明的函數(shù)中使用了errno,前者 用于檢測(cè)數(shù)學(xué)運(yùn)算的合法性,后者用于檢測(cè)I/O操作中(主要是文件)的錯(cuò)誤,例如:
在此程序中,如果文件打開(kāi)失敗(fopen返回NULL),證明發(fā)生了異常。我們讀取error可以獲知錯(cuò)誤的原因,如果D盤根目錄下不存 在"1.txt"文件,將輸出2,表示文件不存在;在文件存在并正確打開(kāi)的情況下,將執(zhí)行到else語(yǔ)句,輸出0,證明errno沒(méi)有被設(shè)置。 Visual C++提供了兩種版本的C運(yùn)行時(shí)庫(kù)。-個(gè)版本供單線程應(yīng)用程序調(diào)用,另一個(gè)版本供多線程應(yīng)用程序調(diào)用。多線程運(yùn)行時(shí)庫(kù)與單線程運(yùn)行時(shí)庫(kù)的一個(gè)重大差別就是 對(duì)于類似errno的全局變量,每個(gè)線程單獨(dú)設(shè)置了一個(gè)。因此,對(duì)于多線程的程序,我們應(yīng)該使用多線程C運(yùn)行時(shí)庫(kù),才能獲得正確的error值。 另外,在使用errno之前,我們最好將其設(shè)置為0,即執(zhí)行errno = 0的賦值語(yǔ)句。 1.4 其它 除了 上述異常處理方式外,在C語(yǔ)言中還支持非局部跳轉(zhuǎn)(使用setjmp和longjmp)、信號(hào)(使用signal、raise)、返回錯(cuò)誤值或回傳錯(cuò)誤值 給參數(shù)等方式進(jìn)行一定能力的異常處理,但是其使用不如1.1~1.3節(jié)所介紹方式常用,我們不必過(guò)細(xì)研究。 從以上分析可知,C語(yǔ)言的 異常處理是簡(jiǎn)單而不全面的。與C++的異常處理比起來(lái),C語(yǔ)言異常處理相形見(jiàn)絀,它就像娘胎里的雛嬰。 2.C++語(yǔ)言異常處理 2.1 C++異常處理語(yǔ)法 感謝C++語(yǔ)言的后期改造者們,他們?cè)跇?biāo)準(zhǔn)C++語(yǔ)言中專門集成了異常處理的相關(guān)語(yǔ)法(與之不同的是,所有的C 標(biāo)準(zhǔn)庫(kù)異常體系都需要運(yùn)行庫(kù)的支持,它不是語(yǔ)言內(nèi)核支持的)。當(dāng)然,異常處理被加到程序設(shè)計(jì)語(yǔ)言中,也是程序語(yǔ)言發(fā)展和逐步完善的必然結(jié)果。我們看 到,C++不是唯一集成異常處理的語(yǔ)言。 C++的異常處理結(jié)構(gòu)為:
而異常 的拋出方式為使用throw(type e),try、catch和throw都是C++為處理異常而添加的關(guān)鍵字。看看這個(gè)例子:
函數(shù)f定義了 兩個(gè)版本:f(int)和f(Point),分別拋出int和Point異常。當(dāng)main函數(shù)的try{…}中調(diào)用f(point)時(shí)和f(1)時(shí),分別 輸出: 捕獲到Point異常:(0,0) 和 捕獲到int異常:1 在 C++中,throw拋出異常的特點(diǎn)有: ?。?)可以拋出基本數(shù)據(jù)類型異常,如int和char等; (2)可以拋 出復(fù)雜數(shù)據(jù)類型異常,如結(jié)構(gòu)體(在C++中結(jié)構(gòu)體也是類)和類; ?。?)C++的異常處理必須由調(diào)用者主動(dòng)檢查。一旦拋出異常,而程序 不捕獲的話,那么abort()函數(shù)就會(huì)被調(diào)用,彈出如圖1所示的對(duì)話框,程序被終止; ?。?)可以在函數(shù)頭后加 throw([type-ID-list])給出異常規(guī)格,聲明其能拋出什么類型的異常。type-ID-list是一個(gè)可選項(xiàng),其中包括了一個(gè)或多個(gè)類 型的名字,它們之間以逗號(hào)分隔。如果函數(shù)沒(méi)有異常規(guī)格指定,則可以拋出任意類型的異常。 2.2 標(biāo)準(zhǔn)異常 下面給 出了C++提供的一些標(biāo)準(zhǔn)異常:
請(qǐng)注意觀察上述類的層次結(jié)構(gòu),可以 看出,標(biāo)準(zhǔn)異常都派生自一個(gè)公共的基類exception?;惏匾亩鄳B(tài)性函數(shù)提供異常描述,可以被重載。下面是exception類的原型:
其中的一個(gè)重要函數(shù)為what(),它返回一個(gè)表示異常的字符串指針。下面我們從exception類派生一個(gè)自己的類:
程序運(yùn)行,輸出: 捕獲到異常:一個(gè)重載exception的例子 一般的,我們直接以基類捕獲異常,例如,本例 中使用了
然后根據(jù)基類的多態(tài)性進(jìn)行 處理,這是因?yàn)榛愔械膚hat函數(shù)是虛函數(shù)。 2.3異常處理函數(shù) 在標(biāo)準(zhǔn)C++中,還定義了數(shù)個(gè)異常處理的相關(guān)函數(shù)和類型(包含在頭 文件<exception>中):
其中的terminate相關(guān)函數(shù)與未被捕獲的異常有關(guān),如果一種異常沒(méi)有被指定catch模塊,則將導(dǎo)致terminate()函數(shù)被調(diào) 用,terminate()函數(shù)中會(huì)調(diào)用ahort()函數(shù)來(lái)終止程序??梢酝ㄟ^(guò)set_terminate(terminate_handler)函數(shù) 為terminate()專門指定要調(diào)用的函數(shù),例如:
這個(gè)程序?qū)⒃诳刂婆_(tái)上輸出 "set_terminate指定的函數(shù)" 字符串,因?yàn)镻oint類型的異常沒(méi)有被捕獲到。當(dāng)然,它也會(huì)彈出圖1所示對(duì)話框(因?yàn)檎{(diào)用了abort()函數(shù))。 上述給出的僅僅 是一個(gè)set_terminate指定函數(shù)的例子。在實(shí)際工程中,往往使用set_terminate指定的函數(shù)進(jìn)行一些清除性的工作,其后再調(diào)用 exit(int)函數(shù)終止程序。這樣,abort()函數(shù)就不會(huì)被調(diào)用了,也不會(huì)輸出圖1所示對(duì)話框。 關(guān)于標(biāo)準(zhǔn)C++的異常處理, 還包含一些比較復(fù)雜的技巧和內(nèi)容,我們可以查閱《more effective C++》的條款9~條款15。 3.MFC異常處理 MFC中異常處理的語(yǔ)法和語(yǔ)義構(gòu)建在標(biāo)準(zhǔn)C++異常 處理語(yǔ)法和語(yǔ)義的基礎(chǔ)之上,其解決方案為: MFC異常處理 = MFC 異常處理類 + 宏 3.1宏 MFC定義了TRY、CATCH(及AND_CATCH、END_CATCH)和THROW(及THROW_LAST)等用于異常處理的宏,其本質(zhì)上也 是標(biāo)準(zhǔn)C++的try、catch和throw的進(jìn)一步強(qiáng)化,由這些宏的定義可知:
這些宏在使用語(yǔ)法上,有如下特點(diǎn): (1)用 TRY 塊包含可能產(chǎn)生異常的代碼; ?。?)用CATCH塊檢測(cè)并處理異常。要注意的是,CATCH塊捕獲到的不是異常對(duì)象,而是指向 異常對(duì)象的指針。此外,MFC靠動(dòng)態(tài)類型來(lái)辨別異常對(duì)象; ?。?)可以在一個(gè)TRY 塊上捆綁多個(gè)異常處理捕獲塊,第一次捕獲使用宏CATCH,以后的使用AND_CATCH,而END_CATCH則用來(lái)結(jié)束異常捕獲隊(duì)列; (4)在異常處理程序內(nèi)部,可以用THROW_LAST 再次拋出最近一次捕獲的異常。 3.2 MFC 異常處理類 MFC較好地將異常封裝到CException類及其派生類中,自成體系,下表給出了MFC 提供的預(yù)定義異常:
標(biāo)準(zhǔn)C++的異 常處理可以處理任意類型的異常,而3.1節(jié)的MFC 宏則只能處理CException 的派生類型,下面我們看一個(gè)CFileException的使用例子:
在這個(gè)程序中,如果D盤根目錄下不存在"1.TXT"這個(gè)文件,將拋出CFileException異常,而且錯(cuò)誤原因成員變量m_cause被設(shè)置為 fileNotFound,我們以CATCH( CFileException, e )就可以捕獲到。錯(cuò)誤原因被定義為CFileException中的枚舉(enum),如下:
我們?cè)谑褂肕FC相關(guān)類時(shí),MFC會(huì)自動(dòng)拋出異常,當(dāng)然我們也可以自行在程序中利用AfxThrowXXXException()拋出各種類型的異常, 其中的XXX與前文的MFC異常類表對(duì)應(yīng)。我們看AfxThrowFileException的例子:
在此程序中,我們?cè)赥RY塊自行利用MFC提供的全局函數(shù)AfxThrowFileException拋出了CFileException異常,其后在 CATCH塊抓住了這個(gè)異常。 MFC建議不再使用TRY、CATCH和THROW宏,而是直接使用標(biāo)準(zhǔn)C++的方式。 4.結(jié)構(gòu)化異常處理 結(jié)構(gòu)化異常處理(Structured Exception Handling,簡(jiǎn)稱SEH)是微軟針對(duì)Windows程序異常處理進(jìn)行的擴(kuò)展,在Visual C++中,它同時(shí)支持C和C++語(yǔ)言。SEH不宜與標(biāo)準(zhǔn)C++異常處理和MFC異常處理混用,對(duì)于C++程序,微軟建議使用標(biāo)準(zhǔn)C++的異常處理。 為了支持SEH,Visual C++中定義了四個(gè)關(guān)鍵字(由于這些關(guān)鍵字是非標(biāo)準(zhǔn)關(guān)鍵字,其它編譯器不一定支持),用以擴(kuò)展C 和C++語(yǔ)言: (1)__except ?。?)__finally ?。?)__leave ?。?)__try 其基本語(yǔ)法為:
或:
其執(zhí)行的步驟如下: (1)__try塊被執(zhí)行; ?。?)如果__try塊沒(méi)有出現(xiàn)異常,則執(zhí)行到 __except塊之后;否則,執(zhí)行到__except塊,根據(jù)filter-expression的值決定異常處理方法: a. filter-expression的值為EXCEPTION_CONTINUE_EXECUTION (-1) 恢復(fù)異常,從發(fā)生異 常處下面開(kāi)始執(zhí)行,異常處理函數(shù)本身不被執(zhí)行; b. filter-expression的值為EXCEPTION_CONTINUE_SEARCH (0) 異常不被識(shí)別,拒絕捕獲異 常,繼續(xù)搜索下一個(gè)異常處理函數(shù); c. filter-expression的值為EXCEPTION_EXECUTE_HANDLER (1) 異常被識(shí)別,終止異常,從異 常發(fā)生處開(kāi)始退棧,一路上遇到的終止函數(shù)都被執(zhí)行。 看看這個(gè)例子:
程序的輸出為:
如果我們把__except(puts("in filter"), 1)改為_(kāi)_except(puts("in filter"), 0),程序的輸出將變?yōu)椋?br>
程序的執(zhí)行也告崩潰,彈出如圖3所示的對(duì)話框。
要想這個(gè)程序能正確地執(zhí)行,我們可以在第一個(gè) __try塊的外面再套一個(gè)__try塊和一個(gè)接收f(shuō)ilter-expression返回值為EXCEPTION_EXECUTE_HANDLER的 __except塊,程序改為:
程序輸出:
由此可以看出,因?yàn)榈谝粋€(gè)__except的filter-expression返回EXCEPTION_CONTINUE_SEARCH 的原因,"in except1"沒(méi)有被輸出。程序之所以沒(méi)有崩潰,是因?yàn)樽罱K碰到了接收EXCEPTION_EXECUTE_HANDLER的第2個(gè)__except。 SEH使用復(fù)雜的地方在于較難控制異常處理的流動(dòng)方向,弄不好程序就"掛"了。如果把例4-1中的__except(puts("in filter"), 1)改為_(kāi)_except(puts("in filter"), -1),程序會(huì)進(jìn)入一個(gè)死循環(huán),輸出:
最后瘋狂地輸出"in filter",我們把斷點(diǎn)設(shè)置在__except(puts("in filter"), -1)語(yǔ)句之前,按F5會(huì)不斷進(jìn)入此斷點(diǎn)。 5.各種異常處理的對(duì)比 下表給出了從各個(gè)方面對(duì)這本文所給出的 Visual C++所支持的四種異常處理進(jìn)行的對(duì)比:
本文所講解的僅僅是Visual C++異常處理的初步知識(shí),對(duì)于更深入的內(nèi)容,還需要我們?cè)诓粩嗟木幊踢^(guò)程中去領(lǐng)悟和學(xué)習(xí)。 在程序設(shè)計(jì)過(guò)程中,我們不能嫌異常處理" 麻煩",對(duì)可能的錯(cuò)誤視而不見(jiàn)、不加考慮。因?yàn)楸苊饬水惓L幚淼?麻煩",將會(huì)給我們的程序帶來(lái)更大的"麻煩"。而程序中包含必要的異常處理,也是對(duì)一位 優(yōu)秀程序員的基本要求。 (#) |
|