【C++】C++异常机制

C++异常

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

  • 终止程序,如assert直接断言报错,缺陷:非常麻烦,如果发生内存错误,除零错误会立即终止程序

  • 返回错误码。缺陷:需要程序员自己去查渣哦对应的错误,如系统库的接口函数都是通过错误码放到errno中,需要程序员自己去读区错误码进行错误处理

  • C标准库中的setjmp和longjmp组合(不常用)

实际上C语言基本都是使用返回错误码来处理错误,部分情况下使用终止程序处理非常严重的错误

C++错误处理方式

C++可以使用异常来对错误进行处理,异常是面向对象语言常用的错误处理方式,当一个函数发现自己出现无法处理的错误的时候就可以抛出异常,让该函数的直接或间接调用者处理这个错误

  • throw : 当程序出现错误,可以通过throw关键字抛出一个异常
  • try : try块中防治当是可能抛出异常的代码,该代码块在执行时会进行异常错误检测,try块后面通常会多跟一个catch块
  • catch : 如果try中发生错误,那么就会跳到对应的catch块执行对应的代码
try {
				// 可能出错的代码
}
catch (ExceptionName e1) {
				// catch 块1
}
catch (ExceptionName e2) {
        // catch 块2
}
// 每一个catch块对应一种错误

不同的catch块对应一种不同的错误

异常的使用方法

异常抛出和捕获的匹配原则

1、异常是通过抛出对象引发的,该对象类型决定应该被哪一个catch模块捕获(有点像函数重载🤔️),如果抛出的异常对象没有被捕获,或是没有匹配类型的捕获,那么这个程序会终止报错

2、被选中的处理代码(catch块)时调用链中与该对象类型匹配且距离抛出异常位置最近的那个

3、抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝对象的临时对象会在catch后被销毁(类似函数的传值返回)

4、catch(…)可以捕获任意类型的异常,但是捕获后无法知道异常错误是什么

5、实际上异常的抛出和捕获的匹配原则有一个例外,捕获和抛出的异常类型并不一定要完全匹配,可以抛出派生类对象,使用基类对象进行捕获,这个在实际生产中使用的很多

函数调用链中异常展开的匹配规则

1、当异常被抛出后,首先会检查throw是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,就跳到catch地方进行处理

2、如果当前函数栈没有匹配的catch则会退出当前的函数栈,返回上一个函数调用栈进行匹配catch。找到匹配的字句进行处理后,会沿着catch字句后面继续执行,不会再回到原来抛出异常的地方

3、如果到达main函数的栈仍然没有找到匹配的catch,则会终止程序

void func3() {
    throw string("异常来了");
}
void func2() {
    try{
        func3();
    } catch (const string& s) {						// 捕获string类型的异常
        cout << "func2 catch :" << s << endl;
        throw int(1);
    }
}
void func1() { func2(); }

int main() {
    try{
        func1();
    } catch (const string& s) {   				// 用于捕获string类型的异常
        cout << "出错啦 : " << s << endl;
    } catch (...) {												// 用于捕获非string类型的异常
        cout << "出现未知错误" << endl;
    }
    cout << "main 函数结束了" << endl;
    return 0;
}

可以看到func1,func2,func3依次被调用,在func3中抛出了一个异常,但是func3中并没有try块,也没有捕获程序,就会退回上一个函数调用栈(func)中查找,可以看到func3()是在try块中的,第一个步骤,检查throw是否在try内部成立,接下来查找匹配的throw字句

然后查看func2内部的catch块类型(因为其是最近的),发现类型是匹配的,就捕获了这个异常,然后又抛出了一个int类型的异常,最终在main函数处被捕获。被捕获后继续执行后续代码

这个沿用调用链查找匹配catch字句的过程称为栈展开,实际过程中最后都要加上一个catch(…)捕获任意类型的异常,否则异常没有被捕获,程序就会被终止

异常的使用规范

有时候单个catch不能完全处理一个异常,在进行一些矫正处理后,希望将异常再交给更外层的调用链函数进行处理,比如最外层可能需要拿到异常进行日志信息的记录,这就要重新抛出异常递交给更上层的函数处理

void func2() {
    throw string("这是个异常");
}

void func1() {
    int* arr = new int[10];
    func2();
    delete[] arr;
}

int main() {
    try {
       func1();
    } catch(const string& s) {
        cout << "捕获字符串异常:" << s << endl;
    } catch(...) {
        cout << "捕获未知异常" << endl;
    }
  return 0}

可以看到这段代码有一点小问题,在函数func1中,使用new在堆上开辟了一块四十字节的空间,之后调用func2函数,func2函数抛出异常被main函数的catch块捕获,然后执行后续代码,main函数结束

可以看到,从始至终,我们在堆上开辟的arr空间并没有被delete,造成了内存泄漏

void func1() {
    int* arr = new int[10];
    try{
        func2();
    } catch (const string& s) {
        delete[] arr;
        cout << "func 2 get a 异常" << endl;
        string func2_exception = "func2" + s;
        throw string(func2_exception);
    }
}

对代码进行简单修改,可以看到我们将delete[] arr的操作放在了func1函数的catch块中,成功对func1函数遗留下的问题进行处理,然后将异常再次抛出

如果这个异常需要加入这个函数的信息,我们可以重新构建异常信息,如果不需要我们可以直接一下结构进行抛出


try {
		func2();
} catch (...) {   // 捕获任意类型异常  
		delete[] arr;
		throw;				// 直接抛出让外层处理
}

异常安全问题

由抛异常导致的安全问题叫做异常安全问题,对于异常安全问题下面有几点建议

1、构造函数完成对象构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整,没有完整出实话,析构函数同理

2、C++中异常经常会出现资源泄漏的问题,如在new和delete之间的代码抛出异常,导致内存泄漏,在lock和unlock之间抛出异常会导致死锁,C++中使用RALL的方式来解决该问题

异常规范

为了能让函数使用者知道某个函数会抛出哪一些异常,C++标准规定:

1、在函数后面接throw(type1, type2, …) 列出这个函数可能抛出的所有异常类型,便于检查理解

2、在函数后面接throw() 或 noexcept(C++11),标识该函数不会抛出异常

3、异常接口声明不是强制的

void func()1 throw(A, B, C, D);			// 可能抛出A,B,C,D类型的异常
void func()2 throw(std::bad_alloc); //只会抛出bad_alloc类型的异常
void func()3 throw();								// 不会抛出异常

自定义异常体系

实际上很多公司都会自定义自己的异常体系进行规范异常处理

  • 公司的项目一般会进行模块划分,不同程序猿小组完成不同模块,如果不对异常进行规范,那么负责外层捕获异常的程序猿就很难受了,内部函数抛出的异常类型千奇百怪,都要一一捕获。如果不进行统一很容易出现错误
  • 实际开发场景中,都会定义一套集成的规范异常体系,先定义一个最基础的基异常类,所有人抛出的异常都必须是继承于该异常类的派生类,异常语法规定可以用基类捕获派生类对象,因此最外层只需要捕获基类就可以了

最基础的异常类至少包括错误编号和错误描述两个成员变量,甚至还可以包含当前函数栈帧的调用链等信息。该异常一般还会提供两个成员函数用于获取错误编号和错误描述

class MyException{
public:
    MyException(int _err_id, const string& _err_msg)
        : err_id(_err_id), err_msg(_err_msg)
        {}
        int GetErrid() const { return err_id; }
        virtual string what() const { return err_msg; }
private:
    int err_id;       // 错误编号
    string err_msg;   // 错误描述
};

​ 如果其他模块想要对异常类进行扩展,必须要继承这个基础的异常类,可以在派生类中按需添加成员变量,或者对继承的what函数进行重写,使其能告诉程序猿更多异常信息


class SqlException : public MyException {
public:
    SqlException(int _err_id, const char* _err_msg, const char* _err_sql)
        : MyException(_err_id, _err_msg)
        , err_sql(_err_sql) {}
    virtual string what(){
        string msg = "CacheException: ";
        msg += err_msg;
        msg += "sql 语句:";
        msg += err_sql;
        return msg;
    }
protected:
    std::string err_sql;
};

注意一下:继承体系中成员变量一般都不用私有,不然在子类中不可见。基类Exception中的what函数可以定义成虚函数,方便自类重写,赋予其更强大的效果

STL中的异常体系

C++标准库中的异常也是一个基础的异常体系,其中exception就是异常基类,我们可以在程序中使用这些标准异常

int main() {
    try {
        vector<int> v(10, 5);
        //  这里如果系统内存不够了就会抛异常
         v.reserve(100000000000000);
        // 这里越界也会抛异常
        v.at(10) = 100;
    } catch (const exception& e) {
        cout << e.what() << endl;    // std::bad_alloc
    } catch (...) {
        cout << "Unknow Exception" << endl;
    }
    return 0;
}

在这里插入图片描述

  • exceptino类的what成员函数和西沟函数都定义成了虚函数,方便字类对其进行重写,从而达到多态的效果
  • 日常开发我们也可以继承exception来实现我们自己的异常类,实际上公司都有自己的一套异常体系

异常的优缺点

C++异常的优点:

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

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

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

4、很多测试框架都使用异常,这样可以更好的使用单元测试等进行白盒测试

5、部分函数的使用异常更好处理,比如构造函数没有返回值,不方便使用错误码处理,比如T& operator这样的函数,如果pos越界了就只能使用异常或者终止程序,没有办法通过返回值表示错误

C++异常的缺点

1、异常会导致程序执行流乱跳,并且非常混乱,运行时出错抛异常就会乱跳。导致我们跟踪分析程序时会很困难

2、C++没有垃圾回收机制,资源需要自己管理,有了异常非常容易内存泄漏,出现死锁等异常安全问题

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

4、异常尽量规范使用,否则后果不堪设想,随意抛异常,外层用户苦不堪言。所以异常规范有两点 一、抛出的异常都必须继承于一个基类 二、函数是否抛异常,抛什么异常需要使用throw(), noexcept的方式进行规范

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在进击

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值