C++进阶——异常

发布于:2025-04-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

1、异常的概念及使用

1.1 异常的概念

1.2 异常的抛出和捕获

1.3 栈展开

1.4 查找匹配的处理代码

1.5 异常的重新抛出

1.6 异常的安全问题

1.7 异常的规范

2、标准库的异常(了解)


1、异常的概念及使用

1.1 异常的概念

C语言,出错了,就报错误码,还要去查询错误信息,比较麻烦。错误处理与正常逻辑混杂,容易遗漏检查。

C++,出错了,就(throw)抛出一个异常对象,包含更全面的错误信息,并且(try)检测错误和(catch)处理错误,分开进行

1.2 异常的抛出和捕获

当程序出现问题时,可以通过抛出(throw一个异常对象来触发异常处理机制。该对象的类型及当前的调用链共同决定了哪个catch处理此异常

异常处理流程

  1. 匹配规则
    被选中的处理代码是调用链中与该异常对象类型匹配(通过类型匹配规则),且距离抛出位置最近catch

  2. 信息传递
    通过异常对象成员变量返回what()的字符串传递错误信息

  3. 控制流转移

    • throw执行时其后的语句不再执行

    • 程序throw跳转匹配的catch,该catch可能在当前函数或调用链上游的某个函数中。

    • 控制权转移意味着:

      • 调用链中的函数可能提前退出(函数栈帧提前销毁)(栈展开,Stack Unwinding)。

      • 栈上已构造的局部对象会按创建顺序的逆序销毁(RAII保证)。

  4. 异常对象生命周期

    • 因为异常对象是局部的(类似函数值返回的临时对象),会调用移动构造,(若没有移动构造,就调用拷贝构造)。

    • 移动(或复制)对象在匹配的catch块结束时 销毁

1.3 栈展开

抛出异常后,程序暂停当前函数的执行

如果 throw try 块内部查找匹配的 catch 语句如果匹配的,则跳到 catch 的地方进行处理

如果当前函数没有 try/catch 子句,或者有 catch 子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的 catch 过程被称为栈展开

如果到达 main 函数,依旧没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止程序

如果找到匹配的 catch 子句处理后当前 catch 子句后面的代码继续执行

1.4 查找匹配的处理代码

一般情况下,抛出对象catch类型匹配的,如果有多个类型匹配的,就选择最近的catch

允许一些例外类型转换:

非常量 -> 常量(即权限缩小),

数组->数组元素的指针

函数->函数指针

派生类->基类,这个非常实用

如果到main函数异常仍旧没有被匹配就会终止程序我们是不期望程序终止,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常但是不知道异常错误是什么

#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <ctime>
#include <cstdlib>

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

class Exception {
public:
    Exception(const std::string& errmsg, int id)
        : _errmsg(errmsg), _id(id) {}
    
    virtual std::string what() const {
        return _errmsg;
    }
    
    int getid() const {
        return _id;
    }

protected:
    std::string _errmsg;
    int _id;
};

class SqlException : public Exception {
public:
    SqlException(const std::string& errmsg, int id, const std::string& sql)
        : Exception(errmsg, id), _sql(sql) {}
    
    std::string what() const override {
        std::string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;
        return str;
    }

private:
    const std::string _sql;
};

class CacheException : public Exception {
public:
    CacheException(const std::string& errmsg, int id)
        : Exception(errmsg, id) {}
    
    std::string what() const override {
        std::string str = "CacheException:";
        str += _errmsg;
        return str;
    }
};

class HttpException : public Exception {
public:
    HttpException(const std::string& errmsg, int id, const std::string& type)
        : Exception(errmsg, id), _type(type) {}
    
    std::string what() const override {
        std::string str = "HttpException:";
        str += _type;
        str += ":";
        str += _errmsg;
        return str;
    }

private:
    const std::string _type;
};

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

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

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

int main() {
    srand(time(0));
    while (1) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        try {
            HttpServer();
        }
        catch (const Exception& e) {  // 这里捕获基类,基类对象和派生类对象都可以被捕获
            std::cout << e.what() << std::endl;
        }
        catch (...) {
            std::cout << "Unkown Exception" << std::endl;
        }
    }
    return 0;
}

1.5 异常的重新抛出

有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理其他错误则重新(throw)抛出异常外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。

#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>

using namespace std;

// 下面程序模拟展示了聊天时发送消息,发送失败捕获异常
// 可能在电梯地下室等场景手机信号不好,需要多次尝试
// 如果多次尝试都发送不出去,则需要捕获异常再重新抛出
// 如果不是网络差导致的错误,捕获后也要重新抛出

class Exception {
public:
    Exception(const string& errmsg, int id)
        : _errmsg(errmsg), _id(id) {}
    
    virtual string what() const {
        return _errmsg;
    }
    
    int getid() const {
        return _id;
    }

protected:
    string _errmsg;
    int _id;
};

class HttpException : public Exception {
public:
    HttpException(const string& errmsg, int id, const string& type)
        : Exception(errmsg, id), _type(type) {}
    
    string what() const override {
        string str = "HttpException:";
        str += _type;
        str += ":";
        str += _errmsg;
        return str;
    }

private:
    const string _type;
};

void _SendMsg(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 {
            _SendMsg(s);
            break;
        }
        catch (const Exception& e) {
            // 捕获异常,如果是102号错误(网络不稳定),则重新发送
            // 如果不是102号错误,则将异常重新抛出
            if (e.getid() == 102) {
                // 重试三次以后还是失败,则说明网络太差,重新抛出异常
                if (i == 3) {
                    throw;
                }
                cout << "开始第" << i + 1 << "次重试" << endl;
            }
            else {
                throw;
            }
        }
    }
}

int main() {
    srand(time(0));
    string str;
    while (cin >> str) {
        try {
            SendMsg(str);
        }
        catch (const Exception& e) {
            cout << e.what() << endl << endl;
        }
        catch (...) {
            cout << "Unknown Exception" << endl;
        }
    }
    return 0;
}

如果是102号错误,这个异常不处理,出了catch子句,自动析构,再尝试发送。

1.6 异常的安全问题

异常抛出后后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。解决方案:

1. 可以中间捕获异常释放资源后再重新抛出麻烦

2. 后面的智能指针章节讲的RAII方式解决这种问题更方便

其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《Effective C++》第8个条款也专门讲了这个问题,别让异常逃离析构函数。

1.7 异常的规范

对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,有助于简化代码

C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1, 类型2...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。

C++98的方式这种方式过于复杂,实践中并不好用,C++11中进行了简化函数参数列表后面noexcept表示不会抛出异常

编译器不会在编译时检查noexcept,也就是说如果一个函数noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常编译还是会顺利通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数 抛出了异常,程序会调用terminate终止程序

noexcept(expression)还可以作为一个运算符去检测一个表达式 是否会抛出异常可能会则返回false不会就返回true

一般外层处理异常

#include <iostream>
using namespace std;

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

int main()
{
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (...)
    {
        cout << "Unknown Exception" << endl;
    }

    int i = 0;
    cout << noexcept(Divide(1, 2)) << endl; // 0
    cout << noexcept(Divide(1, 0)) << endl; // 0
    cout << noexcept(++i) << endl; // 1

    return 0;
}

2、标准库的异常(了解)

exception - C++ Reference

不好用,公司一般自己实现异常库。


网站公告

今日签到

点亮在社区的每一天
去签到