C 几种异常机制简单讲述
引言
这是关于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
#include <stdio.h> #include <stdlib.h> // 数组打印函数 void aprint(int a[], int n); // 数组移动函数 void convert(int a[], int n, int k); //添加一个数组打印宏, a只能是数组 #define ALEN(a) \ sizeof(a)/sizeof(*a) #define APRINT(a) \ aprint(a, ALEN(a)) /* * 面试问题 假如有数组 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); */ int main(int argc, char* argv[]) { // 实现主体逻辑 int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; APRINT(a); convert(a, ALEN(a), 4); APRINT(a); convert(a, ALEN(a), -4); APRINT(a); return system("pause"); } // 数组打印函数 void aprint(int a[], int n) { int i = -1; printf("now data: "); while (++i < n) printf("%d", a[i]); putchar('\n'); } // 数组移动函数 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很像如下处理
最后加上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
以后有机会还是分享一些实际案例开发吧. 如何设计,如何性能优化.拜拜~~~