1、简介

许多的编程新手对异常处理视而不见,程序里很少考虑异常情况。一部分人甚至根本就不考虑,以为程序总是能以正确的途径运行。譬如我们有的程序设计者调用fopen打开一个文件后,立马就开始进行读写操作,根本就不考虑文件是否正常打开了。在编程过程中恰当地使用异常处理可以增强软件的健壮性。本文将介绍C和C++对于异常处理的一些常用方法。

2、C语言的异常处理

2.1、无条件终止

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

#include <stdio.h>

#include <stdlib.h>

int main(void)

{

printf("this will be executed\n");

exit(EXIT_SUCCESS);

printf("this will not be executed\n");

return 0;

}

输出结果为:

this will be executed

在这个例子中,main函数在输出了“this will be executed”之后就执行了exit函数(此函数原型为void exit(int)),因此,程序不会输出" this will not be executed "。程序中的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 hooked function\n");

}

int main(void)

{

atexit(atExitFunc);

printf("this will be executed\n");

exit(EXIT_SUCCESS);

printf("this will not be executed\n");

return 0;

}

输出结果为:

this will be executed

atexit hooked function

在这个例子中,main函数在输出了“this will be executed”之后就执行了exit函数,因此程序不会输出" this will not be executed ",但在程序退出前会执行atExitFunc函数,输出“atexit hooked function”。

如果把上面例子中的“exit(EXIT_SUCCESS);”语句注释掉,

则输出结果如下:

this will be executed

this will not be executed

atexit hooked function

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

#include <stdio.h>

#include <stdlib.h>

static void atExitFunc1(void)

{

printf("atexit hooked function 1\n");

}

static void atExitFunc2(void)

{

printf("atexit hooked function 2\n");

}

static void atExitFunc3(void)

{

printf("atexit hooked function 3\n");

}

int main(void)

{

atexit(atExitFunc1);

atexit(atExitFunc2);

atexit(atExitFunc3);

return 0;

}

输出结果为:

atexit hooked function 3

atexit hooked function 2

atexit hooked function 1

另一个异常终止函数abort(此函数不带参数,原型为void abort(void))会直接退出程序。例如:

#include <stdio.h>

#include <stdlib.h>

int main(void)

{

printf("this will be executed\n");

abort();                                             //这里不是用exit来退出

printf("this will not be executed\n");

return 0;

}

在Visual C++中的debug模式运行时弹出下图1所示的对话框。

图1

虽然exit 和abort两个函数在概念上是相联系的,但它们的效果不同:

abort():程序异常结束。默认情况下,调用abort()导致运行期诊断和程序自毁。它

可能会也可能不会刷新缓冲区、关闭被打开的文件及删除临时文件,这依赖于你的编译

器的具体实现。

exit():文明地结束程序。除了关闭文件和给运行环境返回一个状态码外,exit()还调

用了你挂接的atexit()处理程序。

一般调用abort()处理灾难性的程序故障。因为abort()的默认行为是立即终止程序,

你就必须负责在调用abort()前存储重要数据。

相反,exit()会执行用atexit()挂接的函数,你可以把作挂接的函数当做虚拟析构器。执行必要的clean up 代码,你可以安全地终止程序而没有留下尾巴。

2.2、有条件终止

abort()和exit()让你无条件终止程序。你还可以有条件地终止程序。其实现体系是每

个程序员所喜爱的诊断工具:断言,定义于<assert.h>。 assert宏在C语言程序的调试中发挥着重要的作用,它用于检测不会发生的情况,表明一旦发生了这样的情况,程序就实际上执行错误了。先来看看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宏实际上什么都不做,所以在assert宏中的表达式不能有副作用。看如下的例子:

for(int i=10;i>-5;i++)

{

……

assert((i--)!=0)         //这个语句在debug模式下可以起到递减i的作用,但不在

//debug模式下时,assert宏什么都不做,递减i的作用将失效

……

}

在debug模式下,assert宏实际上是对_assert()函数的调用,这个函数通常有如下形式的定义:

void _assert(int test, char const *test_image,

char const *file, int line)

{

if (!test)

{

printf("Assertion failed: %s, file %s, line %d\n",

test_image, file, line);

abort();

}

}

所以,失败的断言在调用abort()前显示出失败情况的诊断条件、出错的源文件名称和

行号。它们实际上是一个带说明信息的abort()并做了前提条件检查,如果检查失败,程序中止。例如下列程序:

#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;

}

代码中包含断言assert( (strDest != NULL) && (strSrc != NULL) ),它的意思是源和目的字符串的地址都不能为空,一旦为空,程序实际上就执行错误了,会引发一个abort。

失败的断言也会弹出如图1所示的对话框,这是因为_assert()函数中也调用了abort()函数,并且控制台输出结果如图2:

图2

2.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\n", errno);

}

else

{

printf("%d\n", errno);

}

return 0;

}

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

Visual C++提供了两种版本的C运行时库。一个版本供单线程应用程序调用,另一个版本供多线程应用程序调用。多线程运行时库与单线程运行时库的一个重大差别就是对于类似errno的全局变量,每个线程单独设置了一个。因此,对于多线程的程序,我们应该使用多线程C运行时库,才能获得正确的error值。

另外,在使用errno之前,我们最好将其设置为0,即执行errno = 0的赋值语句。

2.4、其他

除了上述异常处理方式外,在C语言中还支持非局部跳转(使用setjmp和longjmp)、信号(使用signal、raise)、返回错误值或回传错误值给参数等方式进行一定能力的异常处理,但是其使用不如2.1~2.3节所介绍方式常用,这里就不细研究。

3、C++异常处理

3.1、异常处理语法

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

try

{

//可能引发异常的代码

}

catch(type_1 e)

{

// type_1类型异常处理

}

catch(type_2 e)

{

// type_2类型异常处理

}

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

{

}

异常的抛出方式使用throw(type e),try、catch和throw都是C++为处理异常而添加的关键字。

异常处理的过程:

1、  程序或运行库遇到一个错误状况(在try块中);

2、  抛出一个异常,程序的运行停止于异常点;

3、  开始搜索异常处理函数。搜索沿调用栈向上搜索,搜索结束于找到了一个异常申明与异常对象的静态类型相匹配;

4、  进入相应的异常处理函数;

5、  异常处理函数结束后,跳到此异常处理函数所在的try 块下面最近的一条语句开始执行。

看看这个例子:

#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);

}

printf("terminating, after 'try' block\n");  //异常处理后从这里开始接着

//处理代码。

return 0;

}

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

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

terminating, after 'try' block

捕获到int异常:1

terminating, after 'try' block

C++中,throw抛出异常的特点有:

(1)可以抛出基本数据类型异常,如int和char等。其中;

(2)可以抛出复杂数据类型异常,如结构体(在C++中结构体也是类)和类;

(3)C++的异常处理必须由调用者主动检查。一旦抛出异常,而程序不捕获的话,那么abort()函数就会被调用,弹出如图1所示的对话框,程序被终止;

(4)可以在函数头后加throw([type-ID-list])给出异常规格,声明其能抛出什么类型的异常。type-ID-list是一个可选项,其中包括了一个或多个类型的名字,它们之间以逗号分隔。如果函数没有异常规格指定,则可以抛出任意类型的异常。

3.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函数是虚函数

 3.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()函数就不会被调用了,也不会输出图1所示对话框。

C和C++中的异常处理的更多相关文章

  1. 【repost】JS中的异常处理方法分享

    我们在编写js过程中,难免会遇到一些代码错误问题,需要找出来,有些时候怕因为js问题导致用户体验差,这里给出一些解决方法 js容错语句,就是js出错也不提示错误(防止浏览器右下角有个黄色的三角符号,要 ...

  2. 第65课 C++中的异常处理(下)

    1. C++中的异常处理 (1)catch语句块可以抛出异常 ①catch中获捕的异常可以被重新抛出 ②抛出的异常需要外层的try-catch块来捕获 ③catch(…)块中抛异常的方法是throw; ...

  3. Swift基础--Swift中的异常处理

    Swift中的异常处理 OC中的异常处理:方法的参数要求传入一个error指针地址,方法执行完后,如果有错误,内部会给error赋值 Swift中的异常处理:有throws的方法,就要try起来,然后 ...

  4. ASP.NET Web API 中的异常处理(转载)

    转载地址:ASP.NET Web API 中的异常处理

  5. Struts2中的异常处理

    因为在Action的execute方法声明时就抛出了Exception异常,所以我们无需再execute方法中捕捉异常,仅需在struts.xml 中配置异常处理. 为了使用Struts2的异常处理机 ...

  6. C++中的异常处理(三)

    C++中的异常处理(三) 标签: c++C++异常处理 2012-11-24 23:00 1520人阅读 评论(0) 收藏 举报  分类: 编程常识(2)  版权声明:本文为博主原创文章,未经博主允许 ...

  7. C++中的异常处理(二)

    C++中的异常处理(二) 标签: c++C++异常处理 2012-11-24 20:56 1713人阅读 评论(2) 收藏 举报  分类: C++编程语言(24)  版权声明:本文为博主原创文章,未经 ...

  8. C++中的异常处理(一)

     来自:CSDN 卡尔  后续有C++中的异常处理(二)和C++中的异常处理(三),C++中的异常处理(二)是对动态分配内存后内部发生错误情况的处理方法,C++中的异常处理(三)中是使用时的异常说明. ...

  9. Java中实现异常处理的基础知识

    Java中实现异常处理的基础知识 异常 (Exception):发生于程序执行期间,表明出现了一个非法的运行状况.许多JDK中的方法在检测到非法情况时,都会抛出一个异常对象. 例如:数组越界和被0除. ...

  10. java 中的异常处理

    一. 异常的概念和Java异常体系结构  异常是程序运行过程中出现的错误.本文主要讲授的是Java语言的异常处理.Java语言的异常处理框架,     是Java语言健壮性的一个重要体现. Java把 ...

随机推荐

  1. Balls(poj 3783)

    The classic Two Glass Balls brain-teaser is often posed as: “Given two identical glass spheres, you ...

  2. sql中问号是干什么的??

    第一次在后台 程序中遇到sql语句中的问号: /** * * 方法描述 : 通过账号id更新该账号状态 * @param state 状态 * @param id 账号id */ @Modifying ...

  3. 前端开发之JavaScript基础篇一

    主要内容: 1.JavaScript介绍 2.JavaScript的引入方法和输出及注释 3.javaScript变量和命名规则 4.五种基本数据类型 5.运算符 6.字符串处理 7.数据类型转换   ...

  4. ABAP内表数据和JSON格式互转

    本程序演示ABAP内表数据如何转为JSON格式,以及JSON数据如何放入内表. 注:json字符串格式如:jsonstr = '[ {flag: "0",message: &quo ...

  5. java web 读取配置文件两种方法

    package com.tsinghua.getDataBaseConn; import java.io.IOException;import java.io.InputStream;import j ...

  6. Android开发实战之ViewPager实现向导界面

    当我们更新应用,或者第一次进入应用时都会有一个向导界面,介绍这个app的内容和使用方式. 如果你细心你会发现其实这就是个viewpager,本篇博文将介绍应用的向导界面是如何制作的.希 望本篇博文对你 ...

  7. copy&mutableCopy 浅拷贝(shallow copy)深拷贝 (deep copy)

    写在前面 其实看了这么多,总结一个结论: 拷贝的初衷的目的就是为了:修改原来的对象不能影响到拷贝出来得对象 && 修改拷贝出来的对象也不能影响到原来的对象 所以,如果原来对象就是imm ...

  8. Shiro 集成Spring 使用 redis时 使用redisTemplate替代jedisPool(五)

    1.添加依赖架包: <dependency> <groupId>org.springframework.data</groupId> <artifactId& ...

  9. Sql_server_2014创建数据库自动备份

    Sql_server_2014创建数据库自动备份 程序员的基础教程:菜鸟程序员

  10. VS2017在Release下编译错误C1001

    在使用VS2017编译C程序时,Debug模式下编译链接执行都没有问题,但是一转到Release模式下就出现下列编译链接错误(IDE:VS2017 /VC++/MFC程序,目标平台x86+Win32位 ...