引言
這是關於C中如何使用異常機制的討論.順帶講一講C中魔法函數的setjmp內部機制.
通過它實現高級的異常try...catch. 允許我先扯一段面試題. 對於計算機面試題. 算法題等.覺得還是有意義的.
在你封裝基礎庫的時候會簡單用的.因為大家都會得你也會那是及格.如果你想及格+1的話...
開始吧,題目是這樣的
/*
* 面試問題 假如有數組 int a[] = {1,2,3,4,5,6,7}, n = 7表示長度, k = 2表示移動步長
* 移動結果變成 1234567=> 6712345.
* 同樣假如 k=3移動三步 1234567=> 5671234
* 實現一個移動函數
* void convert(int a[], int n, int k);
*/
的時候基本基本說算法. 這裡主要想通過這個問題引導異常機制上來.直接貼代碼
// 數組移動函數
void
convert(int a[], int n, int k) {
int t=1, i, j;// t是為了數據記錄終止條件的
// 前面兩個是沒有移動的必要, 後面是已經整除形成周期了也不需要移動
if ((!a) || (n <= 1) || !(k %= n)) return;
// 計算最優的k和n
k = k < 0 ? k + n : k;
// 開始真的移動了, 首先確定趟數, 一般趟就夠了,特殊的就是能夠整除的需要多趟
for (i = 0; t<n && i < k; ++i) {
//開始循環了, 結束條件式循環到頭了
for (j = (i + k) % n; j != i; j = (j + k) % n) {
++t;
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
}
關於上面
// 前面兩個是沒有移動的必要, 後面是已經整除形成周期了也不需要移動
if ((!a) || (n <= 1) || !(k %= n)) return;
就是C中最簡單的異常機制聽過事先 if判斷條件達到異常應對作用. 應該是最簡單的異常處理, 也是常用的一種方式.
附錄完整的測試demo.c

大家是不是覺得很簡單. 這就是異常的本質, 分支.
前言
到這裡會再說另一個和if很像如下處理

最後加上default對於所有情況都處理好.也是異常處理的一種手段.
C開發中或者一個程序接口設計中一種很古老的一種設計有異常判斷接口方法通過return 方法處理.
假如我們 需要寫個函數返回兩個數相除的函數.
#define _RT_OK (0) //結果正確的返回宏
#define _RT_EB (-1) //錯誤基類型,所有錯誤都可用它,在不清楚的情況下
#define _RT_EP (-2) //參數錯誤
#define _RT_EM (-3) //內存分配錯誤
#define _RT_EC (-4) //文件已經讀取完畢或表示鏈接關閉
#define _RT_EF (-5) //文件打開失敗
int
div(double a, double b, double *pc){
if(b==0 || !pa) return _RT_EP;
*pc = a / b;
return _RT_OK;
}
上面這種思路是一種復古套路也許只能C接口封裝能夠看見. 特別實誠. 這個技巧值得擁有.
返回函數運行的狀態碼, 通過指針返回最終結果值. 再扯一點.上面接口設計有一個小瑕疵,調用的時候
需要大量的if 判斷. 假如是後端開發. 對於非法請求直接fake. (exit) 不用給前端返回狀態碼. 降低帶寬.
好了到這裡基本上, 簡單開發中異常處理方式簡單都介紹完了. 後面會實現 try ... catch機制.
正文
到這裡我們先看C中異常處理的魔法函數. 一個比goto更跳躍的函數. 支持函數之間跳躍.首先看一種實現的函數聲明
// Function prototypes
int __cdecl setjmp(
_Out_ jmp_buf _Buf
);
__declspec(noreturn) void __cdecl longjmp(
_In_ jmp_buf _Buf,
_In_ int _Value
);
上面看不明白的關鍵字(VS上的)直接忽略, 第一個函數setjmp 設置標志.第一次使用返回0.後面到這裡來了 返回的是longjmp 第二個參數設置的值.
這裡有個未定義現象. 就是千萬不要用 longjmp 返回0 測試代碼
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
// 這裡測試基礎代碼 longjmp(jbf, 0)
int main(void) {
volatile a = 0;
jmp_buf jbf;
int rt;
rt = setjmp(jbf);
if (rt == 0) {
printf("a %d => %d\n", ++a, rt);
if (a == 2)
exit(0);
}
else {
printf("b %d => %d\n", ++a, rt);
}
// 簡單跳躍一下
if (a == 1)
longjmp(jbf, 0);
return system("pause");
}
運行結果就是未定義按照平台而定了.看下面
.
還有一個 jmp_buf 結構
#define _JBLEN 16
#define _JBTYPE int
typedef struct __JUMP_BUFFER
{
unsigned long Ebp;
unsigned long Ebx;
unsigned long Edi;
unsigned long Esi;
unsigned long Esp;
unsigned long Eip;
unsigned long Registration;
unsigned long TryLevel;
unsigned long Cookie;
unsigned long UnwindFunc;
unsigned long UnwindData[6];
} _JUMP_BUFFER;
主要是保存當前寄存器信息讓其longjmp的時候能夠跳轉. 這方面需要匯編知識. 本人不會. 有機會再學.
希望到這裡關於 C的基礎 setjmp longjmp api說清楚了. 下面看一個完整的 處理異常的案例
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
// 帶異常機制的觸發運算
double jmpdiv(jmp_buf jbf, double a, double b);
// 這裡設置異常機制
int main(int argc, char* argv[]){
jmp_buf jbf;
double a = 2.0, b = 0, c;
int i = 0;
// 隨機數
while(++i <= 10) {
printf("%d => ", i);
// 第一次調用setjmp 返回的值就為0
switch(setjmp(jbf)){
case 0:
b = rand() % 5;
c = jmpdiv(jbf, a, b);
// try部分
printf("%lf / %lf = %lf\n", a,b,c);
break;
case -1:
// 相當於catch 部分
fprintf(stderr, "throw new div is zero!\n");
break;
default:
fprintf(stderr, "uncat error!");
}
}
return 0;
}
double
jmpdiv(jmp_buf jbf, double a, double b) {
// 拋出異常
if(b == 0)
longjmp(jbf, -1);
return a/b;
}
不好意思,今天 說的有點水. 講的不好. 畢竟就是2個api. 用法也很固定. 主要C開發用在協程設計上. 異常處理基本if else switch goto都能解決了.
下面列舉一個 關於 try ... catch 封裝成宏的用法
#include <setjmp.h>
/* try 開始操作 */
#define TRYBEGIN(var) {\
jmp_buf var;\
switch (setjmp(var)){\
case 0:
/* catch檢測錯誤值 */
#define CATCH(z) \
break;\
case z:
/* 默認捕捉的錯誤信息. 推薦在CATCH最後,如果CATCH存在的話 */
#define CATCHBASE() \
default:
/* try最後結束標志 */
#define TRYEND \
break;\
}\
}
/* throw 拋出異常 */
#define THROW(var, z) \
longjmp(var, z)
簡單實用如下
TRYBEGIN(jbf) {
puts("第一次運行到這個");
}
CATCH(1) {
}
CATCHBASE() {
}
TRYEND
到這裡. 分享一個簡易的通過setjmp 和 longjmp 實現協程案例
#include <stdio.h>
#include <setjmp.h>
// 函數棧之間跳轉用的變量
jmp_buf _mtask, _ctask;
// 聲明測試接口
void cushion(void);
void child(void);
int i = 0;
// 主邏輯 執行測試
int main(void) {
if(!setjmp(_mtask))
cushion();
for(i=0;i<5;++i) {
puts("Parent");
if(!setjmp(_mtask))
longjmp(_ctask, 1);
}
}
// 聲明測試接口
void
cushion(void) {
char space[1024];
space[1023] = 1;
child();
}
void
child(void) {
for(i=0;i<5;++i) {
puts("Child loop begin");
// 第一次使用setjmp 函數返回0
if(!setjmp(_ctask))
longjmp(_mtask, 1);
puts("Child loop end");
if(!setjmp(_ctask))
longjmp(_mtask, 1);
}
}
我這裡讓其運行幾次之後就直接結束了. 沒上運行截圖了. 對於setjmp 坑還是很多的. 棧區不夠, 變量狀態. 推薦自己
用幾次感受一下. 不需要深究.只需要知道通過 setjmp + longjmp 能夠實現異常機制try catch就可以了. 再扯一點對於finally實現難
一點點. 大家可以結合上面協程. 思考一下也可以實現的.
到這裡 基本上就關於 異常機制的本質就結束了. 分支 + 跳轉
後記
錯誤是難免的, 交流修改.互相提高認識. setjmp.h 的深入可以看
setjmp的正確使用 http://blog.codingnow.com/2010/05/setjmp.html
以後有機會還是分享一些實際案例開發吧. 如何設計,如何性能優化.拜拜~~~