Visual C++中的异常处理浅析(上)

简介:

Visual C++中的异常处理浅析

作者:宋宝华  e-mail:[email]21cnbao@21cn.com[/email]
 
Visual C++ 提供了对C语言、C++语言及MFC的支持,因而其涉及到的异常(exception)处理也包含了这三种类型,即C语言、C++语言和MFC的异常处理。除此之外,微软对CC++的异常处理进行了扩展,提出了结构化异常处理(SEH)的概念,它支持CC++(与之相比,MFC异常处理仅支持C++)。
一个典型的异常处理包含如下几个步骤:
1)程序执行时发生错误;
2)以一个异常对象(最简单的是一个整数)记录错误的原因及相关信息;
3)程序检测到这个错误(读取异常对象);
4)程序决定如何处理错误;
5)进行错误处理,并在此后恢复/终止程序的执行。
C C++MFCSEH在这几个步骤中表现出了不同的特点。本文将对这四种异常处理进行介绍,并对它们进行对比分析。本文例程的调试平台为Visual C++6.0,操作系统为Windows XP,所有程序均调试通过。
在进入正式的讲解之前,先说几句废话。许多的编程新手对异常处理视而不见,程序里很少考虑异常情况。一部分人甚至根本就不考虑,以为程序总是能以正确的途径运行。譬如我们有的程序设计者调用fopen打开一个文件后,立马就开始进行读写操作,根本就不考虑文件是否正常打开了。这种习惯一定要改掉,纵使你再不愿意!这是软件健壮性的需要!异常处理不是浪费时间!
1.C 语言异常处理
1.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_SUCCESSEXIT_FAILURE分别定义为01
对于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 挂接的函数3
atexit 挂接的函数2
atexit 挂接的函数1
Visual C++中,如果以abort函数(此函数不带参数,原型为void   abort(void))终止程序,则会在debug模式运行时弹出如图1所示的对话框。
abort函数终止程序
1.2  断言
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;
}
在此程序中,为了避免我们的strcpyC库中的strcpy重名,将其改为myStrcpy。程序的输出如图2

2 assert的输出
2 assert的输出
失败的断言也会弹出如图1所示的对话框,这是因为 _assert() 函数中也调用了abort()函数
一定要记住的是assert本质上是一个宏,而不是一个函数,因而不能把带有副作用的表达式放入assert的“参数”中。
1.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 的赋值语句
1.4  其它
除了上述异常处理方式外,在 C 语言中还支持非局部跳转(使用 setjmp longjmp )、信号(使用 signal raise )、返回错误值或回传错误值给参数等方式进行一定能力的异常处理,但是其使用不如 1.1~1.3 节所介绍方式常用,我们不必过细研究。
 
从以上分析可知, C 语言的异常处理是简单而不全面的。与 C++ 的异常处理比起来, C 语言异常处理相形见绌,它就像娘胎里的雏婴。
 
2.C++ 语言异常处理
2.1 C++ 异常处理语法
感谢C++语言的后期改造者们,他们在标准C++语言中专门集成了异常处理的相关语法(与之不同的是, 所有的C标准库异常体系都需要运行库的支持,它不是语言内核支持的 )。当然,异常处理被加到程序设计语言中,也是程序语言发展和逐步完善的必然结果。我们看到,C++不是唯一集成异常处理的语言。
C++ 的异常处理结构为:
try
{
// 可能引发异常的代码
}
catch(type_1 e)
{
// type_1 类型异常处理
}
catch(type_2 e)
{
// type_2 类型异常处理
}
catch (...)// 会捕获所有未被捕获的异常,必须最后出现
{
}
而异常的抛出方式为使用throw(type e)trycatchthrow都是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),分别抛出intPoint异常。当main函数的try{…}中调用f(point)时和f(1)时,分别输出:
捕获到Point异常:(0,0)
捕获到int异常:1
C++中,throw抛出异常的特点有:
1)可以抛出基本数据类型异常,如intchar等;
2)可以抛出复杂数据类型异常,如结构体(在C++中结构体也是类)和类;
3C++的异常处理必须由调用者主动检查。一旦抛出异常,而程序不捕获的话,那么abort()函数就会被调用,弹出如图1所示的对话框,程序被终止;
4)可以在函数头后加throw([type-ID-list])给出异常规格,声明其能抛出什么类型的异常。type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。如果函数没有异常规格指定,则可以抛出任意类型的异常。
 
2.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)中有一个空指针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 *what() const throw();
};
其中的一个重要函数为what(),它返回一个表示异常的字符串指针。下面我们从exception类派生一个自己的类:
#include <iostream>
#include <exception>
using namespace std;
 
class myexception:public exception
{
public:
    myexception():exception(" 一个重载exception的例子")
    {
    }
};
 
int main()
{
    try
    {
      throw myexception();  
    }
    catch (exception &r)  // 捕获异常
    {
       cout << " 捕获到异常:" << r.what() << endl;
    } 
    return 0;
}
程序运行,输出:
捕获到异常:一个重载exception的例子
一般的,我们直接以基类捕获异常,例如,本例中使用了
catch (exception &r)
然后根据基类的多态性进行处理,这是因为基类中的what函数是虚函数。
 


 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120815,如需转载请自行联系原作者



相关文章
|
28天前
|
安全 编译器 程序员
【C++ 泛型编程 进阶篇】 C++ 泛型编程 模板与异常处理、模板与友元之间的使用(一)
【C++ 泛型编程 进阶篇】 C++ 泛型编程 模板与异常处理、模板与友元之间的使用
29 1
|
16天前
|
存储 C++
【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)
【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)
|
21天前
|
C++
11. C++异常处理
11. C++异常处理
16 0
11. C++异常处理
|
24天前
|
算法 IDE Java
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
39 0
|
24天前
|
Java API 开发工具
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(三)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
30 0
|
24天前
|
Java 数据处理 数据库
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(二)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
34 0
|
24天前
|
存储 算法 Java
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(一)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
33 0
|
28天前
|
自然语言处理 安全 程序员
【C++ 泛型编程 进阶篇】 C++ 泛型编程 模板与异常处理、模板与友元之间的使用(二)
【C++ 泛型编程 进阶篇】 C++ 泛型编程 模板与异常处理、模板与友元之间的使用
25 0
|
28天前
|
C++
C++ 异常处理
C++ 异常处理
|
30天前
|
设计模式 安全 编译器
【C++ 异常】C++异常处理:掌握高效、健壮代码的秘密武器
【C++ 异常】C++异常处理:掌握高效、健壮代码的秘密武器
54 1

热门文章

最新文章