异常处理

    技术2022-05-11  68

    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 编译环境    较复杂    不推荐       

    最新回复(0)