异常与异常处理初步
在日常程序處理中,很可能發生一些我們意想不到的情況。最典型的情況是,在一次除法或求模運算中,除數是0——這種情況將觸發一個典型的除零異常。異常有兩個來源,一個來自于CPU,另外一種是來自于程序——我們可以拋出一個異常。
在Delphi中,支持兩種處理異常的結構:try...finally...end和try...except...end。許多初學者搞不太清它們的涵義與用法,這里對它們進行一個基本的介紹。
當編譯器遇到try時,會生成一些用于處理異常的指令。首先,要確定指定當異常發生后,將應用程序的執行位置轉到進行下一步處理的代碼,也就是finally段或except段的代碼。接下來,將該地址壓入用于異常處理的棧結構中,以便正確進出try結構。
先說相對簡單一些的except,當異常沒發生時,該段中的內容不會得到執行,而是直接退出該異常結構,繼續執行end后面的代碼。當異常發生時,會根據該段中on...do(如果有的話)來匹配是否屬于要處理該異常。如果不處理的話,該段及后面的代碼都不會得到執行,而是跳到異常棧中下一個指定的代碼位置。如果處理該異常的話,則會執行on...do對應的語句,并且退出異常塊,繼續執行end后的代碼,除非使用raise要求繼續由下一個異常處理代碼進行處理。未指定on...do的情況,則會被編譯器視同對所有的異常進行都處理。
而finally段中的代碼,一定會得到執行,不論異常是否發生。當異常沒有發生時,執行完finally段后退出該異常,end之后的代碼也會得到繼續執行。而當異常發生時,首先會執行finally段的代碼,然后和except不處理異常的情況一樣,跳到異常棧中下一個指定的代碼位置繼續對異常進行處理。這里需要強調的是,finally段的代碼“一定會得到執行”,跟異常是完全無關的——甚至當在try當中使用Exit提前退出該函數時,也會保證在執行finally段中的代碼之后,才會退出該函數。
換種形像的方式來說,except會呑掉指定的異常,利用該段中的相應內容對異常進行相應的處理,以使后面的代碼能夠正確執行。而finally不會呑掉異常,只保證finally段中的內容在任何情況下都會得到正確執行。也許你會產生疑問,為什么finally不呑掉異常呢?因為在實際使用中,即使異常未得到處理,我們仍要保證一些代碼能夠執行;而如果呑掉直接異常,編譯無法判斷end后面的內容是否仍然能夠正確運行,因而交給下一個異常處理的代碼進行處理是更加安全的做法。遺憾的是,Delphi并未支持如C#那樣的語法,形成try...except...finally...end的結構,導致許多情況下,仍要進行try結構的嵌套。
正是由于try結構進行了許多對要執行的代碼位置的處理,必須小心處理控制流,以保證用于進行異常處理的代碼能夠匹配執行,因而編譯器對控制和跳轉語句進行了嚴格的限制。在try段和except段中,goto的位置必須也處理該段中;而finally段的限制更多,用于跳出該try...finally...end結構的Break、Continue、Exit都不能使用。
正是因為有了這兩個結構,Delphi有了強大的語法支持,可以安全完成許多功能。前面提到的finally的典型情況是用于清理try前分配的資源,例如申請了一塊內存,為保證不出現內存泄露,把相應的釋放代碼寫在finally段中。當在函數中使用局部變量用于臨時分配資源后,使用try...finally...end結構用于執行后續操作和釋放是非常好的編程習慣,建議你在閱讀本文后將所有這種情況的代碼都改成這樣。而except用于從異常中恢復,使程序得到正確的執行,例如處理整數運算的溢出(Overflow)。整數類型的數據類型是有固定的寬度的,例如Byte值的范圍是0~255,當一個Byte類型的整數進行200+200運算時,運算結果超出了Byte能夠容納的范圍,這種情況就是溢出。這種情況下,CPU會在得到運算結果144(400-256)以外,設置標志寄存器的溢出位。遺憾的是,Delphi并沒有直接獲得該標志位姿態的語法支持,但可以通過另外一種方式檢測到:在工程(Project)選項中打開Runtime Errors->Overflow Checking,或在代碼中使用編譯器開關$Q+或$OVERFLOWCHECKS ON打開溢出異常。當發生整數運算溢出后,RTL會拋出一個溢出異常,這樣就可以發現溢出的發生了。假如我們的程序只能對一定范圍內的整數進行處理,正常情況下不會出現溢出。但用戶也可能輸入一個非常大的數字(比如多輸了一個0),導致錯誤的結果。這種情況下,需要檢測到溢出,并且提示用戶輸入的數字過大。這時就可以使用一個try...except...end結構,對溢出異常進行處理,提示用戶發生了溢出,原因是輸入的數字過大。
這里需要補充一下,計算機對異常的處理是非常復雜的,因而速度也非常慢。例如前面提到的除零異常,它是由CPU觸發的,而除法指令(尤其是整數除法指令)本身的處理速度不是一個確定的值,除零運算是其中最慢的情況之一。這里要再補充一些關于CPU與操作系統的知識。以前提到過,x86保護模式支持從ring 0到ring 3一共4個級別的特權模式,數字越低特權級別越高。操作系統工作在級別0中,特權級別最高,一般稱為內核態;而我們為一般的應用程序編寫的代碼,最終工作在級別3中,被稱為用戶態,只有通過特殊的操作才能進入內核態。如果我沒記錯的話,只有三種方式由用戶態切換到內核態:異常(Exception),中斷(Interrupt)和系統調用。其中,異常是我們正在講的東西;中斷是由硬件觸發的,例如主板上的其它硬件設備;系統調用是前些年才出現的CPU的特殊指令,Intel使用SYSENTER,AMD使用SYSCALL。當異常產生時,CPU會掛起正在執行的線程,保存相關的寄存器等等,然后切換特權級別,進入內核態將異常交給操作系統用于處理異常的代碼進行處理。操作系統會根據異常的情況進行相應的處理,當該異常不屬于操作系統要處理的異常時,會將異常的交給要處理異常的應用程序(例如調試器),或者將異常返回給原來的應用程序。因此,上面提到的溢出處理在實際中一般并不使用,而是采用其它的方法進行變通(例如在加法后,運算結果比任何一個加數更小就足以判斷了),也就是說這個例子實際上不太恰當(但我一時也想不出更好的例子了)。
而try...finally在一些實際上跟異常無關的應用中也有應用。例如在使用WinAPI要完成一個比較復雜的功能時,要調用多個API,它們都返回一個需要在使用后釋放的值,其中任何一個返回非法值時都要提前退出,并且清理前面那些返回值。如果在每一個判斷返回值非法的代碼中,都手寫一堆釋放代碼再Exit的話,不僅非常累,還很可能多一個或者少一個處理,以后如果要修改代碼結構時,工作量也異常巨大。但如果使用了一系列try...finally結構的話,只要一個Exit就可以了,非常方便。
轉載于:https://www.cnblogs.com/egust/articles/1853934.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 制作山寨智能机器人的一些记录 一 *
- 下一篇: 凡事过往 皆为序章 只争朝夕 不负韶华是