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. Python极其简单的分布式异步作业管理系统RQ入门

    Python极其简单的分布式异步作业管理系统RQ入门 原创 2017-08-19 lixing 生信人 Python极其简单的分布式异步作业管理系统RQ入门 1. 什么是Job? Job直译过来就是工 ...

  2. 解决windows上安装TortoiseSVN后不能使用命令行问题

    一般我们安装TortoiseSVN的时候都是一路next安装好之后就右键开始使用.但是有时候我们需要在windows的命令窗口下执行SVN命令.这时候我们就会发现svn help之后显示没svn这个命 ...

  3. 启动Eclipse之后,关闭Maven自动更新

    问题描述: 因为架包的修改,所以Maven需要更新,一启动Eclipse之后,自动更新,由于Maven的架包很多download不下来,就一直卡着的样子,很长时间,什么都做不了. 解决办法: Ecli ...

  4. Python模块学习 ---- datetime

    Python提供了多个内置模块用于操作日期时间,像calendar,time,datetime.time模块我在之前的文章已经有所介绍,它提供的接口与C标准库time.h基本一致.相比于time模块, ...

  5. congst与指针

    指向const的指针 //a pointer to const int;指针指向常量对象,相对本指针而言,不能指针指向的对象的常量,不能通过本指针修改常量对象指针,实际的对象不一定的常量 const指 ...

  6. GetEnumName 枚举名称 字符串

    System.TypInfo.pas System.TypInfo.hpp http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TypIn ...

  7. 谈谈跨平台的app开发 坚定的选择了flutter

    目前市场上,(市场也许用的不对),比较常见的技术有xamrin.RN.Flutter,确切的说flutter是后起之秀,笔者也是最近才开始学习,xamrin是微软系的技术,笔者也很早就开始学习了,RN ...

  8. TrinityCore3.3.5编译过程-官方指导-踩坑总结

    官方指导:主页->how to compile -> windows 指导文档写得很详细,但有不少细节点没提到,这里把过程简化总结,说明重点,及易坑点 1,安装需求 编译工具:cmake, ...

  9. Nginx源码完全注释(6)core/murmurhash

    下面是摘自 Google Code 的 Murmurhash 开源项目主页上的 Murmurhash2,Nginx 就是采用的这个. uint32_t MurmurHash2 ( const void ...

  10. 小程序开发运营必看:微信小程序平台运营规范

    一.原则及相关说明 ​ 微信最核心的价值,就是连接——提供一对一.一对多和多对多的连接方式,从而实现人与人.人与智能终端.人与社交化娱乐.人与硬件设备的连接,同时连接服务.资讯.商业. ​ 微信团队一 ...