C++异常处理机制详解

1. 异常的基本概念

1.1 什么是异常

异常是程序在运行时可能发生的意外情况,如除零错误、内存不足、文件不存在等。与C语言通过错误码处理错误不同,C++的异常机制通过抛出对象来表示错误,这种方式能携带更丰富的错误信息。

1.2 异常的抛出和捕获

异常处理涉及三个关键字:

  • throw:抛出异常
  • try:尝试执行可能抛出异常的代码块
  • catch:捕获并处理异常

异常处理机制:

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常。
  • 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
  • throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同一函数中的一个局部的catch,也可能是调用链中另一个函数中的catch,控制权从throw位置转移到了catch位置。这里还有两个重要的含义:1、沿着调用链的函数可能提早退出。2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch子句后销毁。(这里的处理类似于函数的传值返回)
double Divide(int a, int b) {
    if (b == 0) {
        string s("Divide by zero condition!");
        throw s;  // 抛出异常
    }
    return (double)a / (double)b;
}

int main() {
    try {
        cout << Divide(10, 0) << endl;
    }
    catch (const string& errmsg) {  // 捕获异常
        cout << errmsg << endl;
    }
    return 0;
}

2. 栈展开机制

当异常被抛出时,程序会暂停当前函数的执行,开始寻找匹配的catch子句。这个过程称为"栈展开":

  1. 首先检查throw是否在try块内部
  2. 如果是,查找匹配的catch语句
  3. 如果没有找到匹配的catch,则退出当前函数,在外层调用函数中继续查找
  4. 如果到达main函数仍未找到匹配的catch,程序调用terminate终止
    在这里插入图片描述
void func1() { throw string("Error occurred"); }
void func2() { func1(); }
void func3() { func2(); }

int main() {
    try {
        func3();
    }
    catch (const string& errmsg) {
        cout << errmsg << endl;
    }
    return 0;
}

3. 异常继承体系

在实际项目中,通常会定义一个异常基类,然后为不同模块派生特定的异常类。这样可以统一异常处理,同时保留各模块特有的错误信息。

  • 一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
  • 但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。
  • 如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。
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 SqlException : public Exception {
public:
    SqlException(const string& errmsg, int id, const string& sql)
        : Exception(errmsg, id), _sql(sql) {}
    
    virtual string what() const {
        return "SqlException:" + _errmsg + "->" + _sql;
    }

private:
    const string _sql;
};

// 使用示例
void SQLMgr() {
    if (rand() % 7 == 0) {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
}

4. 异常重新抛出

有时我们需要在捕获异常后进行部分处理,然后将异常继续传递给上层调用者:

void SendMsg(const string& s) {
    for (size_t i = 0; i < 4; i++) {
        try {
            _SeedMsg(s);
            break;
        }
        catch (const Exception& e) {
            if (e.getid() == 102) {  // 网络不稳定,重试
                if (i == 3) throw;  // 重试3次后仍失败,重新抛出
                cout << "开始第" << i + 1 << "重试" << endl;
            }
            else {
                throw;  // 其他错误直接重新抛出
            }
        }
    }
}

5. 异常安全

异常安全是指当异常发生时,程序能保持一致性状态,不泄漏资源。常见问题包括:

  1. 内存泄漏:异常发生时已分配的内存未释放
  2. 资源泄漏:文件句柄、锁等资源未释放

解决方案:

  • 使用RAII(资源获取即初始化)技术
  • 在析构函数中避免抛出异常
void Func() {
    int* array = new int[10];
    try {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...) {
        delete[] array;  // 捕获异常时释放内存
        throw;           // 重新抛出异常
    }
    delete[] array;      // 正常情况释放内存
}

6. 异常规范

C++11引入了noexcept关键字来指定函数是否会抛出异常:

// C++98风格
void* operator new (std::size_t size) throw (std::bad_alloc);
void* operator delete (std::size_t size, void* ptr) throw();

// C++11风格
size_type size() const noexcept;
iterator begin() noexcept;

// noexcept运算符
cout << noexcept(Divide(1,2)) << endl;  // 检测表达式是否会抛出异常

7. 标准库异常

C++标准库提供了一套异常类继承体系,基类是std::exception。常用派生类包括:

  • std::bad_alloc:内存分配失败
  • std::bad_cast:动态类型转换失败
  • std::logic_error:程序逻辑错误
  • std::runtime_error:运行时错误
try {
    // 可能抛出标准库异常的代码
}
catch (const std::exception& e) {
    cout << e.what() << endl;  // 获取错误信息
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值