异常处理

C++ 语言异常处理 
Visual C++
提供了对 C 语言、  C++ 语言及 MFC 的支持,因而其涉及到的异常( exception )处理也包含了这三种类型,即 C 语言、 C++ 语言和 MFC 的异常处理。除此之外,微软对 C C++ 的异常处理进行了扩展,提出了结构化异常处理( SEH )的概念,它支持 C C++ (与之相比, MFC 异常处理仅支持 C++ )。  

    一个典型的异常处理包含如下几个步骤:  
    1 )程序执行时发生 错误  
    2 )以一个异常对象(最简单的是一个整数)记录 错误 的原因及相关信息;  
    3 )程序检测到这个 错误 (读取异常对象);  
    4 )程序决定如何处理 错误  
    5 )进行 错误 处理,并在此后恢复 / 终止程序的执行。  
    C C++ MFC SEH 在这几个步骤中表现出了不同的特点。

C
语言异常处理  

    1  异常终止  
    标准 C 库提供了 abort() exit() 两个函数,它们可以强行终止程序的运行,其声明处于 <stdlib.h> 头文件中。这两个函数本身不能检测异常,但在 C 程序发生异常后经常使用这两个函数进行程序终止。下面的这个例子描述了 exit() 的行为:  

#include <stdio.h> 
#include <stdlib.h> 
int main(void) 

   exit(EXIT_SUCCESS); 
   printf(" 程序不会执行到这里 /n"); 
   return 0; 
}  

    在这个例子中, main 函数一开始就执行了 exit 函数(此函数原型为 void exit(int) ),因此,程序不会输出 " 程序不会执行到这里 " 。程序中的 exit(EXIT_SUCCESS) 表示程序正常结束,与之对应的 exit(EXIT_FAILURE) 表示程序执行 错误 ,只能强行终止。 EXIT_SUCCESS EXIT_FAILURE 分别定义为 0 1  

    对于 exit 函数,我们可以利用 atexit 函数为 exit 事件 " 挂接 " 另外的函数,这种 " 挂接 " 有点类似 Windows 编程中的 " 钩子 " Hook )。譬如:  

#include <stdio.h> 
#include <stdlib.h> 
static void atExitFunc(void) 

   printf("atexit 挂接的函数 /n"); 

int main(void) 

   atexit(atExitFunc); 
   exit(EXIT_SUCCESS); 
   printf(" 程序不会执行到这里 /n"); 
   return 0; 
}  

    程序输出 "atexit 挂接的函数 " 后即终止。来看下面的程序,我们不调用 exit 函数,看看 atexit 挂接的函数会否执行:  

#include <stdio.h> 
#include <stdlib.h> 
static void atExitFunc(void) 

   printf("atexit 挂接的函数 /n"); 

int main(void) 

   atexit(atExitFunc); 
   //exit(EXIT_SUCCESS); 
   printf(" 不调用 exit 函数 /n"); 
   return 0; 
}  

    程序输出:  
    不调用 exit 函数  
    atexit 挂接的函数  

    这说明,即便是我们不调用 exit 函数,当程序本身退出时, atexit 挂接的函数仍然会被执行。  
    atexit 可以被多次执行,并挂接多个函数,这些函数的执行顺序为后挂接的先执行,例如:  

#include <stdio.h> 
#include <stdlib.h> 

static void atExitFunc1(void) 

   printf("atexit 挂接的函数 1/n"); 


static void atExitFunc2(void) 

   printf("atexit 挂接的函数 2/n"); 


static void atExitFunc3(void) 

   printf("atexit 挂接的函数 3/n"); 


int main(void) 

   atexit(atExitFunc1); 
   atexit(atExitFunc2); 
   atexit(atExitFunc3); 
   return 0; 
}  

    输出的结果是:  
     atexit 挂接的函数
     atexit 挂接的函数
     atexit 挂接的函数

    Visual C++ 中,如果以 abort 函数(此函数不带参数,原型为 void abort(void) )终止程序,则会在 debug 模式运行时弹出对话框。 
断言 (assert) 
    assert 宏在 C 语言程序的调试中发挥着重要的作用,它用于检测不会发生的情况,表明一旦发生了这样的情况,程序就实际上执行错误了,例如 strcpy 函数:  
char *strcpy(char *strDest, const char *strSrc) 

   char * address = strDest; 
   assert((strDest != NULL) && (strSrc != NULL)); 
   while ((*strDest++ = *strSrc++) != '/0') 
    ; 
   return address; 
}  
    其中包含断言 assert( (strDest != NULL) && (strSrc != NULL) ) ,它的意思是源和目的字符串的地址都不能为空,一旦为空,程序实际上就执行错误了,会引发一个 abort  

    assert 宏的定义为:  
#ifdef NDEBUG 
#define assert(exp) ((void)0) 
#else 
#ifdef __cplusplus 
extern "C" 

   #endif 

   _CRTIMP void __cdecl _assert(void *, void *, unsigned); 
   #ifdef __cplusplus 

#endif 
#define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) ) 
#endif /* NDEBUG */  
    如果程序不在 debug 模式下, assert 宏实际上什么都不做;而在 debug 模式下,实际上是对 _assert()  函数的调用,此函数将输出发生错误的文件名、代码行、条件表达式。例如下列程序:  
#include <stdio.h> 
#include <stdlib.h> 
#include <assert.h> 
char * myStrcpy( char *strDest, const char *strSrc ) 

   char *address = strDest; 
   assert( (strDest != NULL) && (strSrc != NULL) ); 
   while( (*strDest++ = *strSrc++) != '/0' ); 
    return address; 

int main(void) 

   myStrcpy(NULL,NULL); 
   return 0; 
}  

    在此程序中,为了避免我们的 strcpy C 库中的 strcpy 重名,将其改为 myStrcpy
失败的断言也会弹出对话框,这是因为 _assert() 函数中也调用了 abort() 函数。  
    assert 本质上是一个宏,而不是一个函数,因而不能把带有副作用的表达式放入 assert " 参数 " 中。  

   3 errno 
    errno C 程序中是一个全局变量,这个变量由 C 运行时库函数设置,用户程序需要在程序发生异常时检测之。 C 运行库中主要在 math.h stdio.h 头文件声明的函数中使用了 errno ,前者用于检测数学运算的合法性,后者用于检测 I/O 操作中(主要是文件)的错误,例如:  
#include <errno.h> 
#include <math.h> 
#include <stdio.h> 
int main(void) 

   errno = 0; 
   if (NULL == fopen("d://1.txt", "rb")) 
   { 
    printf("%d", errno); 
   } 
   else 
   { 
    printf("%d", errno); 
   } 
   return 0; 
}  
    在此程序中,如果文件打开失败( fopen 返回 NULL ),证明发生了异常。我们读取 error 可以获知错误的原因,如果 D 盘根目录下不存在 "1.txt" 文件,将输出 2 ,表示文件不存在;在文件存在并正确打开的情况下,将执行到 else 语句,输出 0 ,证明 errno 没有被设置。  

    Visual C++ 提供了两种版本的 C 运行时库。 - 个版本供单线程应用程序调用,另一个版本供多线程应用程序调用。多线程运行时库与单线程运行时库的一个重大差别就是对于类似 errno 的全局变量,每个线程单独设置了一个。因此,对于多线程的程序,我们应该使用多线程 C 运行时库,才能获得正确的 error 值。  
    另外,在使用 errno 之前,我们最好将其设置为 0 ,即执行 errno = 0 的赋值语句。  

    4  其它  
    除了上述异常处理方式外,在 C 语言中还支持非局部跳转(使用 setjmp longjmp )、信号(使用  signal  raise )、返回错误值或回传错误值给参数等方式进行一定能力的异常处理。  
    从以上分析可知, C 语言的异常处理是简单而不全面的。与 C++ 的异常处理比起来, C 语言异常处理相形见绌。 

 C++
异常处理语法  
 
标准 C++ 语言中专门集成了异常处理的相关语法(与之不同的是,所有的 标准库异常体系都需要运行库的支持,它不是语言内核支持的)。当然,异常处理被加到程序设计语言中,也是程序语言发展和逐步完善的必然结果。 C++ 不是唯一集成异常处理的语言。  

 1  C++
的异常处理结构为:  
try 

//
可能引发异常的代码  

catch(type_1 e) 

// type_1
类型异常处理  

catch(type_2 e) 

// type_2
类型异常处理  

catch (...)//
会捕获所有未被捕获的异常,必须最后出现  

}  

    而异常的抛出方式为使用 throw(type e) try catch throw 都是 C++ 为处理异常而添加的关键字。举例如下:  

#include <stdio.h> 
//
定义 Point 结构体(类)  
typedef struct tagPoint 

   int x; 
   int y; 
} Point; 
//
扔出 int 异常的函数  
static void f(int n) 

   throw 1; 


//
扔出 Point 异常的函数  
static void f(Point point) 

   Point p; 
   p.x = 0; 
   p.y = 0; 
   throw p; 


int main() 

   Point point; 
   point.x = 0; 
   point.y = 0; 

   try 
   { 
    f(point); // 抛出 Point 异常  
    f(1); // 抛出 int 异常  
   } 
   catch (int e) 
   { 
    printf(" 捕获到 int 异常: %d/n", e); 
   } 
   catch (Point e) 
   { 
    printf(" 捕获到 Point 异常 :(%d,%d)/n", e.x, e.y); 
   } 

   return 0; 
}  

    函数 f 定义了两个版本: f(int) f(Point) ,分别抛出 int Point 异常。当 main 函数的 try{ } 中调用 f(point) 时和 f(1) 时,分别输出:  

    捕获到 Point 异常 :(0,0) 
    捕获到 int 异常:

    C++ 中, throw 抛出异常的特点有:  
    1 )可以抛出基本数据类型异常,如 int char 等;  
    2 )可以抛出复杂数据类型异常,如结构体(在 C++ 中结构体也是类)和类;  
    3 C++ 的异常处理必须由调用者主动检查。一旦抛出异常,而程序不捕获的话,那么 abort() 函数就会被调用,程序被终止;  
    4 )可以在函数头后加 throw([type-ID-list]) 给出异常规格,声明其能抛出什么类型的异常。 type-ID-list 是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。如果函数没有异常规格指定,则可以抛出任意类型的异常。  

    2  标准异常  

    下面给出了 C++ 提供的一些标准异常:  
namespace std 

   //exception 派生  
   class logic_error; // 逻辑错误 , 在程序运行前可以检测出来  

   //logic_error 派生  
   class domain_error; // 违反了前置条件  
   class invalid_argument; // 指出函数的一个无效参数  
   class length_error; // 指出有一个超过类型 size_t 的最大可表现值长度的对象的企图  
   class out_of_range; // 参数越界  
   class bad_cast; // 在运行时类型识别中有一个无效的 dynamic_cast 表达式  
   class bad_typeid; // 报告在表达试 typeid(*p) 中有一个空指针

   //exception 派生  
   class runtime_error; // 运行时错误 , 仅在程序运行中检测到  

   //runtime_error 派生  
   class range_error; // 违反后置条件  
   class overflow_error; // 报告一个算术溢出  
   class bad_alloc; // 存储分配错误  
}  

    请注意观察上述类的层次结构,可以看出,标准异常都派生自一个公共的基类 exception 。基类包含必要的多态性函数提供异常描述,可以被重载。下面是 exception 类的原型:  

class exception 

   public: 
    exception() throw(); 
    exception(const exception& rhs) throw(); 
    exception& operator=(const exception& rhs) throw(); 
    virtual ~exception() throw(); 
    virtual const char *wh
}

3 异常处理函数  
    在标准 C++ 中,还定义了数个异常处理的相关函数和类型(包含在头文件 <exception> 中):  
namespace std 

   //EH 类型  
   class bad_exception; 
   class exception; 

   typedef void (*terminate_handler)(); 
   typedef void (*unexpected_handler)(); 

   //  函数  
   terminate_handler set_terminate(terminate_handler) throw(); 
   unexpected_handler set_unexpected(unexpected_handler) throw(); 

   void terminate(); 
   void unexpected(); 

   bool uncaught_exception(); 
}  

    其中的 terminate 相关函数与未被捕获的异常有关,如果一种异常没有被指定 catch 模块,则将导致 terminate() 函数被调用, terminate() 函数中会调用 ahort() 函数来终止程序。可以通过 set_terminate(terminate_handler) 函数为 terminate() 专门指定要调用的函数,例如:  

#include <cstdio> 
#include <exception> 
using namespace std; 
//
定义 Point 结构体(类)  
typedef struct tagPoint 

   int x; 
   int y; 
} Point; 
//
扔出 Point 异常的函数  
static void f() 

   Point p; 
   p.x = 0; 
   p.y = 0; 
   throw p; 

//set_terminate
将指定的函数  
void terminateFunc() 

   printf("set_terminate 指定的函数 /n"); 


int main() 

   set_terminate(terminateFunc); 
   try 
   { 
    f(); // 抛出 Point 异常  
   } 
   catch (int) // 捕获 int 异常  
   { 
    printf(" 捕获到 int 异常 "); 
   } 
   //Point 将不能被捕获到 , 引发 terminateFunc 函数被执行  

   return 0; 
}  

    这个程序将在控制台上输出  "set_terminate 指定的函数 字符串,因为 Point 类型的异常没有被捕获到。当然,它也会弹出图 1 所示对话框(因为调用了 abort() 函数)。  

    上述给出的仅仅是一个 set_terminate 指定函数的例子。在实际工程中,往往使用 set_terminate 指定的函数进行一些清除性的工作,其后再调用 exit(int) 函数终止程序。这样, abort() 函数就不会被调用了,也不会输出对话框。  

MFC
异常处理  
    MFC 中异常处理的语法和语义构建在标准 C++ 异常处理语法和语义的基础之上,其解决方案为:  
    MFC 异常处理  = MFC  异常处理类     
 1
 
    MFC 定义了 TRY CATCH (及 AND_CATCH END_CATCH )和 THROW (及 THROW_LAST )等用于异常处理的宏,其本质上也是标准 C++ try catch throw 的进一步强化,由这些宏的定义可知:  
#ifndef _AFX_OLD_EXCEPTIONS 

#define TRY { AFX_EXCEPTION_LINK _afxExceptionLink; try { 

#define CATCH(class, e) } catch (class* e) / 
{ ASSERT(e->IsKindOf(RUNTIME_CLASS(class))); / 
_afxExceptionLink.m_pException = e; 

#define AND_CATCH(class, e) } catch (class* e) / 
{ ASSERT(e->IsKindOf(RUNTIME_CLASS(class))); / 
_afxExceptionLink.m_pException = e; 

#define END_CATCH } } 

#define THROW(e) throw e 
#define THROW_LAST() (AfxThrowLastCleanup(), throw) 

// Advanced macros for smaller code 
#define CATCH_ALL(e) } catch (CException* e) / 
{ { ASSERT(e->IsKindOf(RUNTIME_CLASS(CException))); / 
_afxExceptionLink.m_pException = e; 

#define AND_CATCH_ALL(e) } catch (CException* e) / 
{ { ASSERT(e->IsKindOf(RUNTIME_CLASS(CException))); / 
_afxExceptionLink.m_pException = e; 

#define END_CATCH_ALL } } } 

#define END_TRY } catch (CException* e) / 
{ ASSERT(e->IsKindOf(RUNTIME_CLASS(CException))); / 
_afxExceptionLink.m_pException = e; } }  
    这些宏在使用语法上,有如下特点:  
    1 )用 TRY  块包含可能产生异常的代码;  
    2 )用 CATCH 块检测并处理异常。要注意的是, CATCH 块捕获到的不是异常对象,而是指向异常对象的指针。此外, MFC 靠动态类型来辨别异常对象;  
    3 )可以在一个 TRY  块上捆绑多个异常处理捕获块,第一次捕获使用宏 CATCH ,以后的使用 AND_CATCH ,而 END_CATCH 则用来结束异常捕获队列;  
    4 )在异常处理程序内部,可以用 THROW_LAST  再次抛出最近一次捕获的异常。 
 
  2 MFC  异常处理类  
    MFC 较好地将异常封装到 CException 类及其派生类中,自成体系,下表给出了 MFC  提供的预定义异常:  
异常类    含义   
CMemoryException  
内存不足   
CFileException  
文件异常   
CArchiveException  
存档 / 序列化异常   
CNotSupportedException  
响应对不支持服务的请求   
CResourceException  Windows 
资源分配异常   
CDaoException  
数据库异常( DAO  类)   
CDBException  
数据库异常( ODBC  类)   
COleException  OLE 
异常   
COleDispatchException  
调度(自动化)异常   
CUserException  
用消息框警告用户然后引发一般  CException  的异常   

    标准 C++ 的异常处理可以处理任意类型的异常,而上节的 MFC  宏则只能处理 CException  的派生类型,下面我们看一个 CFileException 的使用例子:  

#include <iostream.h> 
#include "afxwin.h" 

int main() 

   TRY 
   { 
    CFile f( "d://1.txt", CFile::modeWrite ); 
   } 
   CATCH( CFileException, e ) 
   { 
    if( e->m_cause == CFileException::fileNotFound ) 
     cout << "ERROR: File not found/n" << endl; 
}
}
要想这个程序能正确地执行,我们可以在第一个 __try 块的外面再套一个 __try 块和一个接收 filter-expression 返回值为 EXCEPTION_EXECUTE_HANDLER __except 块,程序改为:  

#include "stdio.h" 

void main() 

   int* p = NULL; //  定义一个空指针  
   puts("SEH begin"); 
   __try 
   { 
    __try 
    { 
     puts("in try"); 
     __try 
     { 
      puts("in try"); 
      *p = 0; //  引发一个内存访问异常  
     } 
     __finally 
     { 
      puts("in finally"); 
     } 
    } 
    __except(puts("in filter"), 0) 
    { 
     puts("in except"); 
    } 
   } 
   __except(puts("in filter"), 1) 
   { 
    puts("in except"); 
   } 
   puts("SEH end"); 
}  

    程序输出: 
SEH begin 
in try // 执行 __try  
in try //
执行嵌入的 __try  
in filter1 //
执行 filter-expression ,返回 EXCEPTION_CONTINUE_SEARCH 
in filter2 //
执行 filter-expression ,返回 EXCEPTION_EXECUTE_HANDLER 
in finally //
展开嵌入的 __finally 
in except2 //
执行对应的 __except  
SEH end //
处理完毕   
    由此可以看出,因为第一个 __except filter-expression 返回 EXCEPTION_CONTINUE_SEARCH  的原因, "in except1" 没有被输出。程序之所以没有崩溃,是因为最终碰到了接收 EXCEPTION_EXECUTE_HANDLER 的第 2 __except  

    SEH 使用复杂的地方在于较难控制异常处理的流动方向,弄不好程序就 " " 了。如果把例 4-1 中的 __except(puts("in filter"), 1) 改为 __except(puts("in filter"), -1) ,程序会进入一个死循环,输出:  

SEH begin 
in try //
执行 __try  
in try //
执行嵌入的 __try  
in filter //
执行 filter-expression ,返回 EXCEPTION_CONTINUE_EXECUTION 
in filter 
in filter 
in filter 
in filter 
// 疯狂输出 "in filter"  
    最后疯狂地输出 "in filter" ,我们把断点设置在 __except(puts("in filter"), -1) 语句之前,按 F5 会不断进入此断点。 
  各种异常处理的对比  
    下表给出了从各个方面对这本文所给出的 Visual C++ 所支持的四种异常处理进行的对比:  
    
异常处理    支持语言    是否标准    复杂度    推荐使用   
       C
异常处理   C 语言    标准 C   简单    推荐   
       C++
异常处理   C++ 语言    标准 C++   较简单    推荐   
       MFC
异常处理   C++ 语言    仅针对 MFC 程序    较简单    不推荐   
       SEH
异常处理   C C++ 语言    仅针对 Microsoft 编译环境    较复杂    不推荐   
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值