搜索

C++异常处理

Dec 13, 2016 pm 02:38 PM
c++异常处理

引言

异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。而传统错误处理技术,检查到一个局部无法处理的问题时:

1.终止程序(例如atol,atoi,输入NULL,会产生段错误,导致程序异常退出,如果没有core文件,找问题的人一定会发疯)

2.返回一个表示错误的值(很多系统函数都是这样,例如malloc,内存不足,分配失败,返回NULL指针)

3.返回一个合法值,让程序处于某种非法的状态(最坑爹的东西,有些第三方库真会这样)

4.调用一个预先准备好在出现"错误"的情况下用的函数。

第一种情况是不允许的,无条件终止程序的库无法运用到不能当机的程序里。第二种情况,比较常用,但是有时不合适,例如返回错误码是int,每个调用都要检查错误值,极不方便,也容易让程序规模加倍(但是要精确控制逻辑,我觉得这种方式不错)。第三种情况,很容易误导调用者,万一调用者没有去检查全局变量errno或者通过其他方式检查错误,那是一个灾难,而且这种方式在并发的情况下不能很好工作。至于第四种情况,本人觉得比较少用,而且回调的代码不该多出现。

使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。

但是,错误的处理依然是一件很困难的事情,C++的异常机制为程序员提供了一种处理错误的方式,使程序员可以更自然的方式处理错误。

异常实战入门

假设我们写一个程序,把用户输入的两个字符串转换为整数,相加输出,一般我们会这么写

char *str1 = "1", *str2 = "2";
int num1 = atoi(str1);
int num2 = atoi(str2);
printf("sum is %d\n", num1 + num2);

假设用户输入的是str1,str2,如果str1和str2都是整数类型的字符串,这段代码是可以正常工作的,但是用户的输入有可能误操作,输入了非法字符,例如

char *str1 = "1", *str2 = "a";
int num1 = atoi(str1);
int num2 = atoi(str2);
printf("sum is %d\n", num1 + num2);

这个时候结果是1,因为atoi(str2)返回0。

如果用户输入是这样:

char *str1 = "1", *str2 = NULL;
int num1 = atoi(str1);
int num2 = atoi(str2);
printf("sum is %d\n", num1 + num2);

那么这段代码会出现段错误,程序异常退出。

atoi我觉得是一个比较危险的函数,如果在一个重要系统中,调用者不知情,传入了一个NULL字符,程序就异常退出了,导致服务中断,或者传入非法字符,结果返回0,代码继续走下去,在复杂的系统中想要定位这个问题,真是很不容易。

所以比较合适的方式,是我们用异常处理改造一个安全的atoi方法,叫parseNumber。

class NumberParseException {};
bool isNumber(char * str) {
     using namespace std;
     if (str == NULL) 
        return false;
     int len = strlen(str);
     if (len == 0) 
        return false;
     bool isaNumber = false;
     char ch;
     for (int i = 0; i < len; i++) {
         if (i == 0 && (str[i] == &#39;-&#39; || str[i] == &#39;+&#39;)) 
            continue;
         if (isdigit(str[i])) {
            isaNumber = true;
         } else {
           isaNumber = false;
           break;
         }
     }
     return isaNumber;
}
int parseNumber(char * str) throw(NumberParseException) {
    if (!isNumber(str)) 
       throw NumberParseException();
    return atoi(str);
}

上述代码中NumberParseException是自定义的异常类,当我们检测的时候传入的str不是一个数字时,就抛出一个数字转换异常,让调用者处理错误,这比传入NULL字符串,导致段错误结束程序好得多,调用者可以捕获这个异常,决定是否结束程序,也比传入一个非整数字符串,返回0要好,程序出现错误,却继续无声无息执行下去。

于是我们之前写的代码可以改造如下:

char *str1 = "1", *str2 = NULL;
    try {
        int num1 = parseNumber(str1);
        int num2 = parseNumber(str2);
        printf("sum is %d\n", num1 + num2);
    } catch (NumberParseException) {
        printf("输入不是整数\n");
    }

这段代码的结果是打印出"输入不是整数".假设这段代码是运行在一个游戏统计系统中,系统需要定时从大量文件中统计大量用户进入游戏频道1和游戏频道2的次数,str1代表进入游戏频道1的次数,str2表示进入频道2的次数,如果不是使用异常,当输入是NULL程序会导致整个系统宕机,当输入是非法整数,计算结果全部是错误的,当时程序仍然无声无息"正确执行"。

输入非法,抛出NumberParseException,即使调用者没有考虑输入是非法的,例如是:

 char *str1 = "1", *str2 = "12,";
    int num1 = parseNumber(str1);
    int num2 = parseNumber(str2);
    printf("sum is %d\n", num1 + num2);

就算调用者比较粗心,没有捕获异常,程序运行中会抛出NumberParseException,程序宕机,会留下coredump文件,调用者通过"gdb 程序名 coredump文件",查看程序宕机时的堆栈,就知道程序运行中,出现了非法整数字符,那么他就很快知道问题所在,会学乖,把上述代码改成

char *str1 = "1", *str2 = NULL;
    try {
        int num1 = parseNumber(str1);
        int num2 = parseNumber(str2);
        printf("sum is %d\n", num1 + num2);
    } catch (NumberParseException) {
        printf("输入不是整数\n"); 
        //打印文件的路径,行号,str1,str2等信息足够自己去定位问题所在 
    }

这样,下次程序出现问题时,调用者就可以定位问题所在了,这就是异常的错误处理方式,把错误的发现(parseNumber)和错误的处理(游戏统计代码)分开。

这里介绍了异常的抛出和捕获,还有异常的使用场景,接下来就开始一步步讲解C++异常。

异常的描述

函数和函数可能抛出的异常集合作为函数声明的一部分是有价值的,例如

void f(int a) throw (x2,x3);

表示f()只能抛出两个异常x2,x3,以及这些类型派生的异常,但不会抛出其他异常。如果f函数违反了这个规定,抛出了x2,x3之外的异常,例如x4,那么当函数f抛出x4异常时,
会转换为一个std::unexpected()调用,默认是调用std::terminate(),通常是调用abort()。

如果函数不带异常描述,那么假定他可能抛出任何异常。例如:

int f();  //可能抛出任何异常

带任何异常的函数可以用空表表示:

int g() throw (); // 不会抛出任何异常

捕获异常

捕获异常的代码一般如下:

try {
    throw E();
}
catch (H h) {
     //何时我们可以能到这里呢
}

1.如果H和E是相同的类型

2.如果H是E的基类

3.如果H和E都是指针类型,而且1或者2对它们所引用的类型成立

4.如果H和E都是引用类型,而且1或者2对H所引用的类型成立

从原则上来说,异常在抛出时被复制,我们最后捕获的异常只是原始异常的一个副本,所以我们不应该抛出一个不允许抛出一个不允许复制的异常。

此外,我们可以在用于捕获异常的类型加上const,就像我们可以给函数加上const一样,限制我们,不能去修改捕捉到的那个异常。

还有,捕获异常时如果H和E不是引用类型或者指针类型,而且H是E的基类,那么h对象其实就是H h = E(),最后捕获的异常对象h会丢失E的附加携带信息。

 

异常处理的顺序
我们之前写的parseNumber函数会抛出NumberParseException,这个函数只是判断是否数字才抛出异常,但是没有考虑,但这个字符串表示的整数太大,溢出,抛出异常Overflow.表示如下:

class NumberParseException {};
class Overflow : public NumberParseException {};

假设我们parseNumber函数已经为字符串的整数溢出做了检测,遇到这种情况,会抛出Overflow异常,那么异常捕获代码如下:

char *str1 = "1", *str2 = NULL;
    try {
        int num1 = parseNumber(str1);
        int num2 = parseNumber(str2);
        printf("sum is %d\n", num1 + num2);
    } 
    catch (Overflow) {
        //处理Overflow或者任何由Overflow派生的异常
    }
    catch (NumberParseException) {
         //处理不是Overflow的NumberParseException异常
    }

异常组织这种层次结构对于代码的健壮性很重要,因为库函数发布之后,不可能不加入新的异常,就像我们的parseNumber,第一次发布时只是考虑输入是否一个整数的错误,第二次发布时就考虑了判断输入的一个字符串作为整数是否太大溢出,对于一个函数发布之后不再添加新的异常,几乎所有的库函数都不能接受。

如果没有异常的层次结构,当函数升级加入新的异常描述时,我们可能都要修改代码,为每一处调用这个函数的地方加入对应的catch新的异常语句,这很让你厌烦,程序员也很容易忘记把某个异常加入列表,导致这个异常没有捕获,异常退出。

而有了异常的层次结构,函数升级之后,例如我们的parseNumber加入了Overflow异常描述,函数调用者只需要在自己感兴趣的调用场景加入catch(Overflow),并做处理就行了,如果根据不关心Overflow错误,甚至不用修改代码。

未捕获的异常

如果抛出的异常未被捕捉,那么就会调用函数std::terminate(),默认情况是调用abort,这对于大部分用户是正确选择,特别是排错程序错误的阶段(调用abort会产生coredump文件,coredump文件的使用可以参考博客的"学会用core dump调试程序错误")。

如果我们希望在发生未捕获异常时,保证清理工作,可以在所有真正需要关注的异常处理之外,再在main添加一个捕捉一切的异常处理,例如:

int main() {
    try {
        //...
     }
    catch (std::range_error) {
        cerr << "range error\n";
     } catch (std::bad_alloc) {
        cerr << "new run out of memory\n";
     } catch (...) {
       //..
     }
}

这样就可以捕捉所有的异常,除了那些在全局变量构造和析构的异常(如果要获得控制,唯一方式是set_unexpected)。
其中catch(...)表示捕捉所有异常,一般会在处理代码做一些清理工作。

重新抛出

当我们捕获了一个异常,却发现无法处理,这种情况下,我们会做完局部能够做的事情,然后再一次抛出这个异常,让这个异常在最合适的地方地方处理。例如:

void downloadFileFromServer() {
    try {
          connect_to_server();
          //...
     } 
      catch (NetworkException) {
           if (can_handle_it_completely) {
               //处理网络异常,例如重连
           } else {
                throw;
            }
       }
}

这个函数是从远程服务器下载文件,内部调用连接到远程服务器的函数,但是可能存在着网络异常,如果多次重连无法成功,就把这个网络异常抛出,让上层处理。

重新抛出是采用不带运算对象的throw表示,但是如果重新抛出,又没有异常可以重新抛出,就会调用terminate();

假设NetworkException有两个派生异常叫FtpConnectException和HttpConnectException,调用connect_to_server时是抛出HttpConnectException,那么调用downloadFileFromServer仍然能捕捉到异常HttpConnectException。

标准异常

到了这里,你已经基本会使用异常了,可是如果你是函数开发者,并需要把函数给别人使用,在使用异常时,会涉及到自定义异常类,但是C++标准已经定义了一部分标准异常,请尽可能复用这些异常,标准异常参考http://www.cplusplus.com/reference/std/stdexcept/

虽然C++标准异常比较少,但是作为函数开发者,尽可能还是复用c++标准异常,作为函数调用者就可以少花时间去了解的你自定义的异常类,更好的去调用你开发的函数。

总结

本文只是简单从异常的使用场景,再介绍异常的基本使用方法,一些高级的异常用法没有罗列,详细资料可以参考c++之父的C++程序设计语言的异常处理。


声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
C#.NET:探索核心概念和编程基础知识C#.NET:探索核心概念和编程基础知识Apr 10, 2025 am 09:32 AM

C#是一种现代、面向对象的编程语言,由微软开发并作为.NET框架的一部分。1.C#支持面向对象编程(OOP),包括封装、继承和多态。2.C#中的异步编程通过async和await关键字实现,提高应用的响应性。3.使用LINQ可以简洁地处理数据集合。4.常见错误包括空引用异常和索引超出范围异常,调试技巧包括使用调试器和异常处理。5.性能优化包括使用StringBuilder和避免不必要的装箱和拆箱。

测试C#.NET应用程序:单元,集成和端到端测试测试C#.NET应用程序:单元,集成和端到端测试Apr 09, 2025 am 12:04 AM

C#.NET应用的测试策略包括单元测试、集成测试和端到端测试。1.单元测试确保代码的最小单元独立工作,使用MSTest、NUnit或xUnit框架。2.集成测试验证多个单元组合的功能,常用模拟数据和外部服务。3.端到端测试模拟用户完整操作流程,通常使用Selenium进行自动化测试。

高级C#.NET教程:ACE您的下一次高级开发人员面试高级C#.NET教程:ACE您的下一次高级开发人员面试Apr 08, 2025 am 12:06 AM

C#高级开发者面试需要掌握异步编程、LINQ、.NET框架内部工作原理等核心知识。1.异步编程通过async和await简化操作,提升应用响应性。2.LINQ以SQL风格操作数据,需注意性能。3..NET框架的CLR管理内存,垃圾回收需谨慎使用。

C#.NET面试问题和答案:提高您的专业知识C#.NET面试问题和答案:提高您的专业知识Apr 07, 2025 am 12:01 AM

C#.NET面试问题和答案包括基础知识、核心概念和高级用法。1)基础知识:C#是微软开发的面向对象语言,主要用于.NET框架。2)核心概念:委托和事件允许动态绑定方法,LINQ提供强大查询功能。3)高级用法:异步编程提高响应性,表达式树用于动态代码构建。

使用C#.NET建筑微服务:建筑师实用指南使用C#.NET建筑微服务:建筑师实用指南Apr 06, 2025 am 12:08 AM

C#.NET是构建微服务的热门选择,因为其生态系统强大且支持丰富。1)使用ASP.NETCore创建RESTfulAPI,处理订单创建和查询。2)利用gRPC实现微服务间的高效通信,定义和实现订单服务。3)通过Docker容器化微服务,简化部署和管理。

C#.NET安全性最佳实践:防止常见漏洞C#.NET安全性最佳实践:防止常见漏洞Apr 05, 2025 am 12:01 AM

C#和.NET的安全最佳实践包括输入验证、输出编码、异常处理、以及身份验证和授权。1)使用正则表达式或内置方法验证输入,防止恶意数据进入系统。2)输出编码防止XSS攻击,使用HttpUtility.HtmlEncode方法。3)异常处理避免信息泄露,记录错误但不返回详细信息给用户。4)使用ASP.NETIdentity和Claims-based授权保护应用免受未授权访问。

c语言中:是什么意思c语言中:是什么意思Apr 03, 2025 pm 07:24 PM

C 语言中冒号 (':') 的含义:条件语句:分隔条件表达式和语句块循环语句:分隔初始化、条件和增量表达式宏定义:分隔宏名和宏值单行注释:表示从冒号到行尾的内容为注释数组维数:指定数组的维数

c语言中a  是什么意思c语言中a 是什么意思Apr 03, 2025 pm 07:21 PM

C 语言的 a 是后增运算符,其运作机制包括:先获取变量 a 的值。将 a 的值增加 1。返回自增后的 a 的值。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中