setjmp()和longjmp()函式
之前我們講到了過程活動記錄(AR),那麼如何來操縱AR呢,一個可能的方法是,根據區域性變數的地址進行推算,例如對於上面的a函式,執行a函式時的當前AR地址就是引數i的地址偏移8個位元組,也就是 ((char*)&i) - 8。
然而,不同的C編譯器,以及不同的硬體平臺都會產生不同的AR結構佈局,甚至在一些平臺上,AR根本不會存放到Stack中。所以這種方式操縱AR是不通用的。
為此,C語言通過庫函式的方式提供了操縱AR的統一方法,那就是setjmp和longjmp函式。
(注意:goto語句不能跳出C語言當前的函式)
1. 作用:
setjmp()和longjmp() 可以實現非區域性控制轉移即從一個函式到另外一個函式的跳轉。
2. 函式原型:
int setjmp(jmp_buf j); void longjmp(jmp_buf j, int i);
setjmp函式設定返回點,儲存呼叫函式的棧環境與j中(相當於保護現場)。 l
ongjmp的作用是使用setjmp儲存在j中的棧環境資訊返回到setjmp的位置,也就是當執行longjmp時程式又回到setjmp處(相當於恢復現場)。
setjmp有兩個作用:
1)儲存呼叫函式的棧環境於j中,返回值為0
2)作為longjmp的返回目標地,返回值為longjmp的第二個引數i,使程式碼能夠知道它是實際上是通過longjmp返回的
當然,當使用longjmp()時,j的內容被銷燬。
3. jmp_buf資料型別
typedef struct __jmp_buf_tag jmp_buf[1];
jmp_buf實際是一個數組,記憶體分配在棧空間中,作為引數傳遞時是一個指標(指向呼叫函式的棧幀)。
4. 具體例項
#include <stdio.h> #include <setjmp.h> jmp_buf buf; void haha() { printf("in haha()\n"); longjmp(buf,1); printf("kaonima\n"); } int tim=0; int main() {if(setjmp(buf)) { printf("back in main\n"); tim++; } else { printf("first time through\n"); haha(); } if(tim<3) longjmp(buf,1); return 0; }
執行結果是:
first time through in haha() back in main back in main back in main
6.異常處理
這裡舉一個使用這兩個函式進行異常處理的例子
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> jmp_buf jb; void f1() { printf("進入f1()\n"); if(0/*正確執行*/){ } else { longjmp(jb,1); } printf("退出f1()\n"); } void f2() { printf("進入f2()\n"); if(1/*正確執行*/) { } else { longjmp(jb, 2); } printf("退出f2()\n"); } int main() { int r = setjmp(jb); if(r==0){ f1(); f2(); }else if(r==1){ printf("處理錯誤1\n"); exit(1); }else if(r==2){ printf("處理錯誤2\n"); exit(2); } return 0; }
當然完整的異常處理遠比這裡的程式碼要複雜,需要考慮異常的巢狀等,這裡僅僅給出最簡單的思路。
注:不要在C++中使用setjmp和longjmp
C++為異常處理提供了直接支援。除非極特殊需要,不要再重新實現自己的異常機制,尤其需要說明的是,簡單的呼叫setjmp/longjmp有可能帶來問題。
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> class MyClass { public: MyClass(){ printf("MyClass::MyClass()\n");} ~MyClass(){ printf("MyClass::~MyClass()\n");} }; jmp_buf jb; void f1() { MyClass obj; printf("進入f1()\n"); if(0/*正確執行*/){ } else { longjmp(jb,1); } printf("退出f1()\n"); } void f2() { printf("進入f2()\n"); if(1/*正確執行*/) { } else { longjmp(jb, 2); } printf("退出f2()\n"); } int main() { int r = setjmp(jb); if(r==0){ f1(); f2(); }else if(r==1){ printf("處理錯誤1\n"); exit(1); }else if(r==2){ printf("處理錯誤2\n"); exit(2); } return 0; }
g++編譯,程式輸出:
MyClass::MyClass()
進入f1()
處理錯誤1
vc++編譯,程式輸出:
MyClass::MyClass() 進入f1() MyClass::~MyClass() 處理錯誤1
longjmp()跳轉前區域性物件可能並不會析構(g++),也可能析構(VC++),C++標準對此並無明確要求。這種依賴於具體編譯器版本的程式碼是應該避免的。
而C++本身的throw關鍵字,卻能嚴格保證區域性物件構造和析構的成對呼叫。
參考:
https://blog.csdn.net/smstong/article/details/50728022
https://blog.csdn.net/c1194758555/article/details/52780068
https://blog.csdn.net/qq_33656136/article/details/52732970
C專家程式設計 6.8節