之前我們講到了過程活動記錄(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節