| 將對setjmp與longjmp的具體使用方法和適用的場合,進行一個非常全面的闡述。 另外請?zhí)貏e注意,setjmp函數(shù)與longjmp函數(shù)總是組合起來使用,它們是緊密相關(guān)的一對操作,只有將它們結(jié)合起來使用,才能達到程序控制流有效轉(zhuǎn)移的目的,才能按照程序員的預(yù)先設(shè)計的意圖,去實現(xiàn)對程序中可能出現(xiàn)的異常進行集中處理。 與goto語句的作用類似,它能實現(xiàn)本地的跳轉(zhuǎn) 這種情況容易理解,不過還是列舉出一個示例程序吧!如下: void main( void ) { int jmpret; jmpret = setjmp( mark ); if( jmpret == 0 ) { // 其它代碼的執(zhí)行 // 判斷程序遠行中,是否出現(xiàn)錯誤,如果有錯誤,則跳轉(zhuǎn)! if(1) longjmp(mark, 1); // 其它代碼的執(zhí)行 // 判斷程序遠行中,是否出現(xiàn)錯誤,如果有錯誤,則跳轉(zhuǎn)! if(2) longjmp(mark, 2); // 其它代碼的執(zhí)行 // 判斷程序遠行中,是否出現(xiàn)錯誤,如果有錯誤,則跳轉(zhuǎn)! if(-1) longjmp(mark, -1); // 其它代碼的執(zhí)行 } else { // 錯誤處理模塊 switch (jmpret) { case 1: printf( "Error 1\n"); break; case 2: printf( "Error 2\n"); break; case 3: printf( "Error 3\n"); break; default : printf( "Unknown Error"); break; } exit(0); } return; } 上面的例程非常地簡單,其中程序中使用到了異常處理的機制,這使得程序的代碼非常緊湊、清晰,易于理解。在程序運行過程中,當異常情況出現(xiàn)后,控制流是進行了一個本地跳轉(zhuǎn)(進入到異常處理的代碼模塊,是在同一個函數(shù)的內(nèi)部),這種情況其實也可以用goto語句來予以很好的實現(xiàn),但是,顯然setjmp與 longjmp的方式,更為嚴謹一些,也更為友善。程序的執(zhí)行流如圖17-1所示。 ? setjmp與longjmp相結(jié)合,實現(xiàn)程序的非本地的跳轉(zhuǎn) 呵呵!這就是goto語句所不能實現(xiàn)的。也正因為如此,所以才說在C語言中,setjmp與longjmp相結(jié)合的方式,它提供了真正意義上的異常處理機制。其實上一篇文章中的那個例程,已經(jīng)演示了longjmp函數(shù)的非本地跳轉(zhuǎn)的場景。這里為了更清晰演示本地跳轉(zhuǎn)與非本地跳轉(zhuǎn),這兩者之間的區(qū)別,我們在上面剛才的那個例程基礎(chǔ)上,進行很小的一點改動,代碼如下: void Func1() { // 其它代碼的執(zhí)行 // 判斷程序遠行中,是否出現(xiàn)錯誤,如果有錯誤,則跳轉(zhuǎn)! if(1) longjmp(mark, 1); } void Func2() { // 其它代碼的執(zhí)行 // 判斷程序遠行中,是否出現(xiàn)錯誤,如果有錯誤,則跳轉(zhuǎn)! if(2) longjmp(mark, 2); } void Func3() { // 其它代碼的執(zhí)行 // 判斷程序遠行中,是否出現(xiàn)錯誤,如果有錯誤,則跳轉(zhuǎn)! if(-1) longjmp(mark, -1); } void main( void ) { int jmpret; jmpret = setjmp( mark ); if( jmpret == 0 ) { // 其它代碼的執(zhí)行 // 下面的這些函數(shù)執(zhí)行過程中,有可能出現(xiàn)異常 Func1(); Func2(); Func3(); // 其它代碼的執(zhí)行 } else { // 錯誤處理模塊 switch (jmpret) { case 1: printf( "Error 1\n"); break; case 2: printf( "Error 2\n"); break; case 3: printf( "Error 3\n"); break; default : printf( "Unknown Error"); break; } exit(0); } return; } 回顧一下,這與C++中提供的異常處理模型是不是很相近。異常的傳遞是可以跨越一個或多個函數(shù)。這的確為C程序員提供了一種較完善的異常處理編程的機制或手段。 setjmp和longjmp使用時,需要特別注意的事情 1、setjmp與longjmp結(jié)合使用時,它們必須有嚴格的先后執(zhí)行順序,也即先調(diào)用setjmp函數(shù),之后再調(diào)用longjmp函數(shù),以恢復(fù)到先前被保存的“程序執(zhí)行點”。否則,如果在setjmp調(diào)用之前,執(zhí)行l(wèi)ongjmp函數(shù),將導(dǎo)致程序的執(zhí)行流變的不可預(yù)測,很容易導(dǎo)致程序崩潰而退出。請看示例程序,代碼如下: class Test { public: Test() ~Test() }obj; //注意,上面聲明了一個全局變量obj void main( void ) { int jmpret; // 注意,這里將會導(dǎo)致程序崩潰,無條件退出 Func1(); while(1); jmpret = setjmp( mark ); if( jmpret == 0 ) { // 其它代碼的執(zhí)行 // 下面的這些函數(shù)執(zhí)行過程中,有可能出現(xiàn)異常 Func1(); Func2(); Func3(); // 其它代碼的執(zhí)行 } else { // 錯誤處理模塊 switch (jmpret) { case 1: printf( "Error 1\n"); break; case 2: printf( "Error 2\n"); break; case 3: printf( "Error 3\n"); break; default : printf( "Unknown Error"); break; } exit(0); } return; } 上面的程序運行結(jié)果,如下: 構(gòu)造對象 Press any key to continue 的確,上面程序崩潰了,由于在Func1()函數(shù)內(nèi),調(diào)用了longjmp,但此時程序還沒有調(diào)用setjmp來保存一個程序執(zhí)行點。因此,程序的執(zhí)行流變的不可預(yù)測。這樣導(dǎo)致的程序后果是非常嚴重的,例如說,上面的程序中,有一個對象被構(gòu)造了,但程序崩潰退出時,它的析構(gòu)函數(shù)并沒有被系統(tǒng)來調(diào)用,得以清除一些必要的資源。所以這樣的程序是非常危險的。(另外請注意,上面的程序是一個C++程序,所以大家演示并測試這個例程時,把源文件的擴展名改為 xxx.cpp)。 2、除了要求先調(diào)用setjmp函數(shù),之后再調(diào)用longjmp函數(shù)(也即longjmp必須有對應(yīng)的setjmp函數(shù))之外。另外,還有一個很重要的規(guī)則,那就是longjmp的調(diào)用是有一定域范圍要求的。這未免太抽象了,還是先看一個示例,如下: int Sub_Func() { int jmpret, be_modify; be_modify = 0; jmpret = setjmp( mark ); if( jmpret == 0 ) { // 其它代碼的執(zhí)行 } else { // 錯誤處理模塊 switch (jmpret) { case 1: printf( "Error 1\n"); break; case 2: printf( "Error 2\n"); break; case 3: printf( "Error 3\n"); break; default : printf( "Unknown Error"); break; } //注意這一語句,程序有條件地退出 if (be_modify==0) exit(0); } return jmpret; } void main( void ) { Sub_Func(); // 注意,雖然longjmp的調(diào)用是在setjmp之后,但是它超出了setjmp的作用范圍。 longjmp(mark, 1); } 如果你運行或調(diào)試(單步跟蹤)一下上面程序,發(fā)現(xiàn)它真是挺神奇的,居然longjmp執(zhí)行時,程序還能夠返回到setjmp的執(zhí)行點,程序正常退出。但是這就說明了上面的這個例程的沒有問題嗎?我們對這個程序小改一下,如下: int Sub_Func() { // 注意,這里改動了一點 int be_modify, jmpret; be_modify = 0; jmpret = setjmp( mark ); if( jmpret == 0 ) { // 其它代碼的執(zhí)行 } else { // 錯誤處理模塊 switch (jmpret) { case 1: printf( "Error 1\n"); break; case 2: printf( "Error 2\n"); break; case 3: printf( "Error 3\n"); break; default : printf( "Unknown Error"); break; } //注意這一語句,程序有條件地退出 if (be_modify==0) exit(0); } return jmpret; } void main( void ) { Sub_Func(); // 注意,雖然longjmp的調(diào)用是在setjmp之后,但是它超出了setjmp的作用范圍。 longjmp(mark, 1); } 運行或調(diào)試(單步跟蹤)上面的程序,發(fā)現(xiàn)它崩潰了,為什么?這就是因為,“在調(diào)用setjmp的函數(shù)返回之前,調(diào)用longjmp,否則結(jié)果不可預(yù)料” (這在上一篇文章中已經(jīng)提到過,MSDN中做了特別的說明)。為什么這樣做會導(dǎo)致不可預(yù)料?其實仔細想想,原因也很簡單,那就是因為,當setjmp函數(shù)調(diào)用時,它保存的程序執(zhí)行點環(huán)境,只應(yīng)該在當前的函數(shù)作用域以內(nèi)(或以后)才會有效。如果函數(shù)返回到了上層(或更上層)的函數(shù)環(huán)境中,那么setjmp保存的程序的環(huán)境也將會無效,因為堆棧中的數(shù)據(jù)此時將可能發(fā)生覆蓋,所以當然會導(dǎo)致不可預(yù)料的執(zhí)行后果。 3、不要假象寄存器類型的變量將總會保持不變。在調(diào)用longjmp之后,通過setjmp所返回的控制流中,例程中寄存器類型的變量將不會被恢復(fù)。(MSDN中做了特別的說明,上一篇文章中,這也已經(jīng)提到過)。寄存器類型的變量,是指為了提高程序的運行效率,變量不被保存在內(nèi)存中,而是直接被保存在寄存器中。寄存器類型的變量一般都是臨時變量,在C語言中,通過register定義,或直接嵌入?yún)R編代碼的程序。這種類型的變量一般很少采用,所以在使用setjmp和longjmp時,基本上不用考慮到這一點。 4、MSDN中還做了特別的說明,“在C+ +程序中,小心對setjmp和longjmp的使用,因為setjmp和longjmp并不能很好地支持C++中面向?qū)ο蟮恼Z義。因此在C++程序中,使用C++提供的異常處理機制將會更加安全。”雖然說C++能非常好的兼容C,但是這并非是100%的完全兼容。例如,這里就是一個很好的例子,在C++ 程序中,它不能很好地與setjmp和longjmp和平共處。在后面的一些文章中,有關(guān)專門討論C++如何兼容支持C語言中的異常處理機制時,會做詳細深入的研究,這里暫且跳過。 |