一.什么是异常处理
cpu负责捕捉无效内存访问和用0除一个数值这种错误,并相应的引发一个异常作为对这些错误的反应。cpu引起的异常就是所谓的“硬件异常”。操作系统和应用程序也可以引发异常,称为“软件异常”。
当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察时什么类型的异常被引发,并能够让应用程序来自己处理异常。
通过__except关键字来实现异常处理程序。每当你建立一个__try块,它必须跟随一个finally块或except块。一个try块之后不能既有finally块和except块。但可以再try-except块中嵌套try-finally块,反之亦然。
二.异常过滤器和异常处理程序
和结束处理程序不同,异常过滤器和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和指向异常处理程序方面不作什么,在try块中使用retun,goto等语句也不会引起局部展开的系统开销。
例1
int function()
{
int temp = 0;
__try
{
temp=5/temp; //尝试0除以5 引发硬件异常
temp = 10;
}
__except(EXCEPTION_EXECUTE_HANDLER) //括号中为异常过滤表达式
{
printf("exception!\n");
}
return temp;
}
int main(int argc, char *argv[])
{
int ret = function();
printf("ret = %d\n", ret);
}
try块中有一个指令试图以0来除5。cpu将捕捉这个事件并引发一个硬件异常。当引发异常时,系统将定位到except块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果只能是下面是哪个标识符之一:
本例过滤器表达式的值是EXCEPTION_EXECUTE_HANDLER。这个值的意思是已经感觉这个异常可能在某个时候发生,我已经编写了代码来处理这个问题,现在我想执行这个代码。本例执行结果:
可以看到程序引发硬件异常后,直接跳转到了except块,并执行了其中的代码,但是异常之后的代码并没有被执行。
三.一些例子
例2
char* StringCopy(char* destination, const char* source)
{
int temp = 0;
__try
{
strcpy(destination, source);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//什么都不做
}
return destination;
}
这个函数所做的一切就是讲strcpy的调用放在一个结构化异常处理框架中。如果strcpy执行成功,函数就返回。如果strcpy引起一个存取异常,异常过滤器返回EXCEPTION_EXECUTE_HANDLER,导致该线程执行异常处理代码。在这个函数中,处理程序什么也不做,只是返回到它的调用者,根本不会造成进程结束。
例3
//返回字符串中以空格分割的符号的个数,比如"aaa bbb"会返回2
int RobustHowManyToken(const char* str)
{
int howManyTokens = -1; //返回-1表示函数执行失败
char* temp = NULL;
__try
{
temp = (char*)malloc(strlen(str)+1); //分配临时缓冲
strcpy(temp,str); //将参数拷贝到临时缓冲 strtok会改变它所操作的字符
char* token = strtok(temp, " ");
for(;token!=NULL;token=strtok(NULL," "))
howManyTokens++;
howManyTokens++; //从-1开始 要+1
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//什么都不做
}
free(temp);
return howManyTokens;
}
如果像函数传递NULL(或任何无效的内存地址),howManyTokens被初始化成-1,在try块中对strlen的调用会引起异常。异常过滤器获得控制并将控制转移给excepy块,except什么也不做,在except块之后,调用free释放内存,由于内存未分配,像free传递NULL,NULL作为参数传递给free是合法的,最后返回-1说明函数调用失败,但是进程并没有结束。
假如传递参数是合法的,但是malloc失败了并返回NULL,这将导致strcpy的调用会引起异常。同样异常过滤器被调用,except块被执行,free被调用,返回-1,函数返回失败,进程没有结束。
最后假定调用者像函数传递一个有效的参数,并且malloc也调用成功。这种情况下其余的代码可以正确计算出符合的数量并保存在变量中,异常过滤器不会被求值,except块中的代码不会被执行,临时内存缓冲器将被释放,并向调用者返回howManyTokens。
此函数说明了如何不适用try-finally的情况下保证释放资源。在异常处理程序之后的代码也都能被执行。
例4
//给字符串重新分配内存
unsigned char* RobustMemDup(unsigned char* str, size_t cb)
{
unsigned char* dup = NULL;
__try
{
dup = (unsigned char*)malloc(cb);
memcpy(dup, str, cb);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
free(dup);
dup = NULL;
}
return dup;
}
这个函数分配一个内存缓冲区,并向缓冲器复制内容,然后将复制的内存缓冲区返回,如果失败则返回NULL。希望调用程序在不需要缓冲器时释放他。本例中的except块中有代码。
如果调用程序给参数传一个无效的地址,或者malloc调用失败,memcy会引起一个异常,异常过滤器被调用,except块被执行。在except块中,内存被释放,dup被设置成NULL表示函数失败。
如果调用程序给函数传递一个有效地址,malloc调用成功,则新分配的内存地址将会被返回。
四.全局展开
当一个异常过滤器的值为EXCEPTION_EXECUTE_HANDLER时,系统必须执行一个全局展开。全局展开是由异常引起的,当try块中触发异常,异常处理先查找到离次try层最近的except块,如果过滤器的值为EXCEPTION_EXECUTE_HANDLER,依次先执行上层的finally块代码,然后再执行except块中的代码。
例5
void function()
{
int temp = 0;
__try
{
__try
{
__try
{
printf("step 1\n");
temp = 5/temp; //引发一个异常
}
__finally
{
printf("step 2\n");
}
printf("step 5\n"); //不执行
}
__finally
{
printf("step 3\n");
}
printf("step 5\n"); //不执行
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("step 4\n");
}
}
int main(int argc, char *argv[])
{
function();
}
执行结果:
五.暂停全局展开
通过在finally块中放一个return语句,可以阻止一个全局展开。
例6
void function()
{
int temp = 0;
__try
{
__try
{
__try
{
printf("step 1\n");
temp = 5/temp; //引发一个异常
}
__finally
{
printf("step 2\n");
return; //返回
}
printf("step 5\n"); //不执行
}
__finally
{
printf("step 3\n");
}
printf("step 5\n"); //不执行
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("step 4\n"); //不执行
}
}
int main(int argc, char *argv[])
{
function();
}
执行结果
可以看到停止了全局展开,except中的代码没有执行。
六.EXCEPTION_CONTINUE_EXECUTION
例7
int temp1 = 0;
long OilFilter(int temp)
{
if(temp==0)
{
temp1 = 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_EXECUTE_HANDLER;
}
void function()
{
int temp2 = 6;
__try
{
temp1 = temp2/temp1; //第一次触发异常
printf("%d\n",temp1);
temp2 = 0;
temp1 = temp1/temp2; //第二次触发异常
printf("%d\n",temp1);
}
__except(OilFilter(temp1))
{
printf("except\n");
}
}
int main(int argc, char *argv[])
{
function();
}
在function中,try块中代码第一次尝试除以temp1,此时temp1的值是0,会触发异常并计算except块的异常过滤器。在except块中,将temp1传递给OilFilter,OilFilter中检测传入的temp1的值是0,将temp重新赋值为2,返回EXCEPTION_CONTINUE_EXECUTION,当系统看到过滤器的值是EXCEPTION_CONTINUE_EXECUTION,系统调回到产生异常的指令,试图再执行一次。这一个指令执行成功,temp1得到正确的结果。
后面代码再次计算,这次再次遇到除以0的问题,不过被除数是temp2,这一场OilFilter看到temp1的值不是0,返回EXCEPTION_EXECUTE_HANDLER,这是告诉系统去执行except块中的代码。
执行结果:
但是这样使用需要特别注意,有可能在某些特殊情况下,造成死循环,所以使用EXCEPTION_CONTINUE_EXECUTION时需要特别小心。
七.EXCEPTION_CONTINUE_SEARCH
这个标识符的意思是,告诉系统去查找前面与一个except块相匹配的try块。
例8
void function()
{
int temp = 0;
__try
{
__try
{
temp=6/temp;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("except1\n");
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("except2\n");
}
}
int main(int argc, char *argv[])
{
function();
}
执行结果:
如果程序修改为
void function()
{
int temp = 0;
__try
{
__try
{
temp=6/temp;
}
__except(EXCEPTION_CONTINUE_SEARCH) //修改为EXCEPTION_CONTINUE_SEARCH
{
printf("except1\n");
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("except2\n");
}
}
int main(int argc, char *argv[])
{
function();
}
执行结果