欢迎光临
我们一直在努力

【C++】异常

异常

1、C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误,异常使问题更简单,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分

C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出⼀个对象,这个对象可以函数更全面的各种信息

2、异常的抛出与捕获

  • throw:当程序出现问题时,我们通过抛出(throw)一个对象引发一个异常
  • catch:在想处理问题的地方,通过异常处理程序捕获异常,catch是专门用来接住、处理throw抛出来的异常错误的代码块
  • try:try块中的代码标识将被激活的特定异常,它后面通常跟一个或者多个catch块

3、异常的使用

3.1 栈展开

抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句

  • 首先检查throw本身是否在try块内部,如果在就查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理
  • 如果当前没有匹配的try/catch子句则退出当前的函数栈,或者有try/catch子句但类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开
  • 如果达到main函数的栈,依旧没有匹配的catch子句,程序会终止
  • 如果找到匹配的catch子句处理后,catch子句代码会继续执行

在这里插入图片描述

如图的三个函数func1()、func2()、func3()。在func2()中调用func1(),func3()中调用func2(),main()中调用func3(),并在func1()中抛出一个异常,在main()中用catch语句捕获

double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
else
{
return ((double)a / (double)b);
}

cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
return 0;
}

void Func()
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}

int main()
{
try
{
Func();
}
catch (const string& errmsg)
{
cout << errmsg << endl;
}
catch (int errid)
{
cout << errid << endl;
}

return 0;
}

输入1 0

输出结果:

Divide by zero condition!

栈展开的过程:

  • b==0,当前函数终止,throw之后的代码不会执行,Divide函数内没有try-catch捕获,触发栈展开
  • 回到Func函数,Func中调用Divide的语句被异常打断,后面的cout语句不会执行,Func函数中无try-catch,继续栈展开,销毁Func栈帧
  • 异常继续向上抛,回到main函数的try{}代码块内
  • main函数try块捕捉异常,匹配catch
  • 一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个

    在Divide函数中加入try-catch

    double Divide(int a, int b)
    {
    try
    {
    // 当b == 0时抛出异常
    if (b == 0) // 输入1 0
    {
    string s("Divide by zero condition!");
    throw s; // 抛出异常
    }
    else
    {
    return ((double)a / (double)b);
    }

    //… fxx()
    }
    catch (const int& s)
    {
    cout << s << endl;
    }
    catch (const string& errmsg)// 抛出string,类型匹配,本catch块执行
    {
    cout << errmsg << endl;
    }

    // 本地catch捕获异常,栈展开终止,继续向下执行函数剩余代码
    cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
    return 0; // 返回0给上层Func,本行执行
    }

    void Func()
    {
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;// 调用Divide拿到返回值0,输出0,执行
    cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl; // 无异常抛出,本行正常执行
    }

    int main()
    {
    try
    {
    Func();
    }
    // 异常被Divide本地捕获,不会进入main的catch,不执行
    catch (const string& errmsg)
    {
    cout << errmsg << endl;
    }
    catch (int errid)
    {
    cout << errid << endl;
    }

    return 0;
    }

    输入1 0

    输出结果:

    Divide by zero condition!
    Divide:30行执行
    0
    Func:42行执行

    在Divide函数中已有try-catch模块,并且离异常最近,不需要main函数中的catch块

    注意:throw抛出异常后try内的其余代码不会执行了,如果本函数内有匹配的catch接住异常,那么try-catch结构之外、函数剩下的代码会正常执行,如果本函数内没有匹配的catch接住异常,那么剩下的所有代码都不会执行

    eg:

    try
    {
    throw s; // 直接跳出try,try内剩下代码全不跑
    // 下面这些永远不执行
    else分支return;
    //…fxx();
    }
    catch(string& err)
    {
    cout << err; // 捕获异常,执行完这个块
    }
    // try-catch外面的代码不受异常影响,正常运行
    cout << __FUNCTION__ << "执行" << endl;
    return 0;

    try
    {
    string s;
    throw s; // 抛出string
    }
    catch(const int&) // 只能捕获int,匹配失败
    {
    }
    // 下面两行【完全不会执行】,直接退出函数向上抛异常
    cout << __FUNCTION__ << "执行" << endl;
    return 0;

    3.2 查找匹配的处理代码

    #include<thread>

    /*一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块
    每个模块的继承都是Exception的派生类,每个模块可以添加自己的数据
    最后捕获时,我们捕获基类就可以*/

    // 父类 Exception(所有异常的通用模板)
    class Exception
    {
    public:
    // 构造函数:接收错误描述、错误编号
    Exception(const string& errmsg, int id)
    :_errmsg(errmsg)
    , _id(id)
    { }

    // virtual虚函数:打印异常信息,子类会重写这个函数
    virtual string what() const
    {
    return _errmsg;
    }

    // 获取错误编号
    int getid() const
    {
    return _id;
    }
    protected:
    string _errmsg;
    int _id;
    };

    // 构造:除了父类要的errmsg、id,额外接收出错SQL语句
    class SqlException : public Exception
    {
    public:
    SqlException(const string& errmsg, int id, const string& sql)
    :Exception(errmsg, id)
    , _sql(sql)
    { }

    // 重写what(),拼接专属报错格式
    virtual string what() const
    {
    string str = "SqlException:";
    str += _errmsg;
    str += "->";
    str += _sql;
    return str;
    }
    private:
    const string _sql; //
    };

    class CacheException : public Exception
    {
    public:
    CacheException(const string& errmsg, int id)
    :Exception(errmsg, id)
    {
    }

    virtual string what() const
    {
    string str = "CacheException:";
    str += _errmsg;
    return str;
    }
    };

    class HttpException : public Exception
    {
    public:
    HttpException(const string& errmsg, int id, const string& type)
    :Exception(errmsg, id)
    , _type(type)
    {
    }

    virtual string what() const
    {
    string str = "HttpException:";
    str += _type;
    str += ":";
    str += _errmsg;
    return str;
    }
    private:
    const string _type;
    };

    void SQLMgr()
    {
    if (rand() % 7 == 0)
    {
    throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
    else
    {
    cout << "SQLMgr 调用成功" << endl;
    }
    }

    void CacheMgr()
    {
    if (rand() % 5 == 0)
    {
    throw CacheException("权限不足", 100);
    }
    else if (rand() % 6 == 0)
    {
    throw CacheException("数据不存在", 101);
    }
    else
    {
    cout << "CacheMgr 调用成功" << endl;
    }

    SQLMgr();
    }

    void HttpServer()
    {
    if (rand() % 3 == 0)
    {
    throw HttpException("请求资源不存在", 100, "get");
    }
    else if (rand() % 4 == 0)
    {
    throw HttpException("权限不足", 101, "post");
    }
    else
    {
    cout << "HttpServer调用成功" << endl;
    }

    CacheMgr();
    }

    // 健壮性
    int main()
    {
    srand(time(0));
    while (1) // 无限循环,持续模拟服务运行
    {
    this_thread::sleep_for(chrono::seconds(1));

    try
    {
    HttpServer(); // 所有业务逻辑入口
    }
    catch (const Exception& e) // 这里捕获基类,基类对象和派生类对象都可以被捕获
    {
    cout << e.what() << endl; // 多态
    }
    catch (...) // 兜底捕获任意其他异常
    {
    cout << "未知异常" << endl;
    }
    // …
    }

    return 0;
    }

    上面的代码中父类:Exception是通用异常模板,所有模块报错都继承它,三个业务派生异常:SqlException:数据库 SQL 报错、CacheException:缓存模块报错、HttpException:网络HTTP请求报错,额外存请求方式

    允许从派生类向基类类型的转换:抛出子类的对象,catch用父类的引用/指针可以直接匹配

    4、异常重新抛出

    有可能单个的catch不能完全处理一个异常,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理

    // 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在
    // 电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发
    // 送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的
    // 错误,捕获后也要重新抛出。
    void _SeedMsg(const string& s)
    {
    if (rand() % 2 == 0)
    {
    throw HttpException("网络不稳定,发送失败", 102, "put");
    }
    else if (rand() % 7 == 0)
    {
    throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
    }
    else
    {
    cout << "发送成功" << endl;
    }
    }

    void SendMsg(const string& s)
    {
    //发送消息失败,则再重试3次
    for (size_t i = 0; i < 4; i++)
    {
    try
    {
    _SeedMsg(s);
    break;
    }
    catch (const Exception& e)
    {
    //捕获异常,if中是102,网络不稳定
    //捕获异常,else中不是102号错误,则将异常重新抛出
    if (e.getid() == 102)
    {
    //重试三次以后否失败了,则说明网络太差了,重新抛出异常
    if (i == 3)
    throw;
    cout << "开始第" << i + 1 << "重试" << endl;
    }
    else
    {
    throw;
    }
    }
    }
    }

    int main()
    {
    srand(time(0));
    string str = "xxxxxxxx";
    while (cin>>str)
    {
    //this_thread::sleep_for(chrono::seconds(1));

    try
    {
    SendMsg(str);
    }
    catch (const Exception& e)
    {
    cout << e.what() << endl << endl;
    }
    catch (...)
    {
    cout << "Unkown Exception" << endl;
    }
    }

    return 0;
    }

    在上面的代码中,发送消息如果失败会重新发送三次消息,如果三次发送都失败那么捕捉异常

    4、异常安全

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

    其次析构函数中,最好不要在析构函数中抛出异常,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了

    5、异常规范

    • 对于用户和编译器而言,预先知道某个程序会不会抛出异常会有很大帮助,有助于简化调用函数的代码
    • C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2…)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割

    // C++98写法:承诺本函数不抛异常
    int Add(int a, int b) throw()
    {
    return a + b;
    }

    // 声明:本函数最多只抛string、int两种异常
    void Test(int x) throw(std::string, int)
    {
    if (x == 0)
    throw std::string("除零错误");
    if (x < 0)
    throw 1;
    // 不能抛出string/int以外的类型,否则运行时出错
    }

    • C++11后,一个函数如果不抛异常,就在函数参数列表后面加noexcept,但是⼀个声明了noexcept的函数抛出了异常,程序会调用terminate 终止程序
    • noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会就返回true

    #include <iostream>
    #include <string>
    using namespace std;

    // 标注noexcept:承诺无异常
    void SafeFunc() noexcept
    {
    cout << "安全函数,不会报错" << endl;
    // 下面放开注释,运行直接崩溃终止程序
    // throw string("我违规抛异常了");
    }

    int main()
    {
    try
    {
    SafeFunc();
    }
    catch(...)
    {
    cout << "捕获异常" << endl;
    }
    return 0;
    }

    正常运行输出:安全函数,不会报错

    如果取消throw注释:不会进入 catch,程序直接闪退终止

    6、异常的优缺点

    6.1 优点

  • 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的 bug
  • 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator[]这样的函数,如果 pos 越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误
  • 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常
  • 6.2 缺点

  • 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难
  • 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计
  • C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高
  • C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱
  • 赞(0)
    未经允许不得转载:171主机测评 » 【C++】异常
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址