C++·异常

1. C语言传统的处理错误方式

        assert终止程序:一般用来断言一些难以接受的错误,如发生内存错误。

        返回错误码:文件打开失败,会返回一个errno错误码,这个错误码需要程序员自己去查找对应的错误,或者使用perror自动分析错误码打印出错误码对应的错误原因。

        perror的用法参考:C语言·字符函数和字符串函数-CSDN博客,在最后strerror的讲解中有错误码和perror的讲解

2. C++异常概念

        异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接调用者处理这个错误。

        throw:当问题出现时,程序会抛出一个异常。这是使用throw关键字来完成的

        catch:catch用于捕获从下面抛出来的异常

        try:try块中存放可能会抛出异常的函数或代码块,它后面通常跟着一个或多个catch块

        如果有一个块抛出一个异常,捕获异常的方法会使用try和catch关键字。try块中放置可能抛出异常的代码,try块中的代码被称为保护代码。

3. 异常的使用

3.1 异常的抛出和捕获

        下面我们观察一下抛异常的规则

        catch(...) 可以捕获任意类型的异常,如果抛出的异常没有被任何别的catch捕获到。

        在throw抛出异常后会沿着函数栈帧一层一层的向外查看是不是在try中,如果在就去对应的catch查找有没有能对应到捕获异常的catch比如,上面代码中抛出的异常是字符串,就匹配catch char*的,如果抛出的是整形数字就去匹配能catch int型的。

        这个类型一定是严格匹配的,不能走隐式类型转换,比如抛出一个 1 ,这个1就是int型的,那外面用 size_t 去catch都是捕获不到的。

        但是实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出派生类对象,用基类捕获,这个在实际应用中非常实用。

        抛出异常后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象。这个拷贝出来或移动出来的对象会在catch解析完之后销毁。这个过程就像抛出异常之后传值返回到匹配的catch中去,也就是说如果有移动构造throw回去的时候就会使用移动构造,如果没有才是拷贝构造。

        在抛出异常后便不会继续执行下面的代码,而是一层一层的寻找try,如果找到了框住了这段代码try则寻找try对应的catch有没有能匹配异常的,如果没有则继续向外破出函数栈帧寻找try。直到找到能够匹配的 try-catch ,找到之后抛出catch中反应异常的语句,之后才会从try块之后继续运行。

                ​​​​​​​        ​​​​​​​        

        如果达到main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着栈调链查找匹配的catch句子的过程叫做栈展开。所以实际中我们都要最后加上一个catch(...)捕获任意类型的异常,否则当有异常没被捕获,程序就会直接终止。

        在栈展开的过程中,会将函数栈帧清理干净的,比如调用声明周期在该栈帧中的自定义类型的析构函数。但是这并没有完全解决潜在存在的内存泄露问题。

3.2 异常的重新抛出

        ​​​​​​​        ​​​​​​​        

        我们看上面这段代码,如果没有引入抛异常之前,这段代码是没有问题的。但是现在这个Division函数是有可能抛异常的,如果真的抛异常了,那势必会造成array1的内存泄漏。

        ​​​​​​​        ​​​​​​​                ​​​​​​​

        因此我们这里采用强行接收任何异常的方案确保不会出现内存泄漏的问题,catch任何异常都去抛出,因为最外面我们写了一段分析异常的代码。

        但这也没有完全解决这一问题

        ​​​​​​​        

        如果又new了一个array2,同时在这个位置抛异常了,也就是开空间失败了,那array1明显就会内存泄漏了,那可不可以再给array2再包一层try-catch,当然可以,那如果new了很多呢,我们无法把每个都包上,因此此时就要借助智能指针来解决这个问题了。

        只能指针我们下节就详细展开,它就是可以在函数栈帧结束的时候连带着把它指向的内容都析构清理掉的,因此我们只要给array1、array2包上只能指针就可以完全避免内存泄露的问题了。无论是从哪个位置抛出了异常,只要出了这个函数的栈帧,那这两块空间都会析构掉。

3.3 异常安全

        构造函数完成对象的构造和初始化,最好不要在构造函数中抛异常,否则可能会导致对象构造不完整或没有完全初始化。

        析构函数主要完成资源的清理,最好也不要在析构函数内抛异常,否则可能导致资源泄露

        C++中异常经常会导致资源泄露的问题,比如在new和delete之间抛异常,导致内存泄露,在lock和unlock之间抛异常导致锁死

3.4 异常规范

        C++11之前会在函数后面加上throw(类型)来标识这个函数可能会抛出什么类型的异常,如果函数后面接throw()就表明这个函数一定不会抛异常。

        在C++11之后加入关键字 noexcept 加在函数后面表示这个函数一定不会抛异常,而如果会抛异常的函数后面就什么都不写了。

4. 异常的优缺点

异常的优点:

        1. 异常对象定义好了,相比错误码的方式可以更清晰准确展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug

        2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们就要层层返回错误,最外层才能拿到错误

        3. 很多第三方库中都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。

        4. 部分函数使用异常更好处理。

异常的缺点:

        1. 异常会导致程序执行流乱跳,非常混乱。这会导致我们跟踪调试分析程序的时候非常困难。

        2. C++不像Java,C++没有垃圾回收机制,资源需要自己管理。有了异常就容易导致内存泄漏、锁死等异常安全问题。这个需要使用RAII来处理资源管理的问题。有一定学习成本。

        3. C++标准库的异常体系定义的不好,导致大家各自定义各自的异常体系,非常混乱

        4. 异常尽量规范使用,否则后果严重,外层捕获会苦不堪言。所以异常规范有两点:一、抛出的异常都继承自一个基类。二、函数是否抛异常,抛什么异常都使用func() throw() 的方式规范化。

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值