C++异常机制:抛出、捕获与栈展开

错误处理是程序里绕不开的话题。C语言用错误码,函数返回一个int,调用方去查表,麻烦不说,还容易把业务逻辑和错误处理搅在一起。C++的异常机制提供了一种不同的思路:把发现错误处理错误的代码分开。出错的模块只负责抛出异常,至于怎么处理,由调用链上合适的捕获点决定,两边互不侵入。

这篇文章把异常的抛出、捕获、栈展开过程以及类型匹配规则梳理一遍,最后用一组自定义异常体系展示实际项目中怎么用。

目录

[1. 抛出与捕获的基本姿势](#1. 抛出与捕获的基本姿势)

[1.1 throw干了什么](#1.1 throw干了什么)

[1.2 try/catch怎么接](#1.2 try/catch怎么接)

[2. 栈展开:异常的传播路径](#2. 栈展开:异常的传播路径)

[3. 自定义异常体系:利用派生类到基类的转换](#3. 自定义异常体系:利用派生类到基类的转换)

小结


1. 抛出与捕获的基本姿势

1.1 throw干了什么

当程序运行到throw语句时,会构造一个异常对象,然后函数的剩余代码不再执行,控制权开始沿着调用链向上转移,寻找匹配的catch。这个过程有三个关键点:

  • throw后面的语句被跳过。

  • 当前函数可能提前退出。

  • 局部对象会按顺序析构(栈展开时完成)。

throw的异常对象如果是局部的,会生成一份拷贝交给catch,原对象在栈展开过程中销毁。这份拷贝会在catch处理完毕后销毁。

1.2 try/catch怎么接

cpp

复制代码
void Func() {
    int len, time;
    cin >> len >> time;
    try {
        cout << Divide(len, time) << endl;
    } catch (const char* errmsg) {
        cout << errmsg << endl;
    }
    cout << "Func continuing..." << endl;
}

double Divide(int a, int b) {
    if (b == 0) {
        string s("Divide by zero condition!");
        throw s;
    }
    return (double)a / (double)b;
}

Divide里抛出的是string对象,而Func里捕获的类型是const char*,不匹配。所以Func的catch会被跳过,继续向外层调用者(这里是main)寻找匹配的catch(string)。如果main也找不到,程序就调用terminate终止。

catch的匹配规则

  • 一般情况下要求类型完全匹配。

  • 允许从非常量向常量的转换(权限缩小方向),但不允许其他隐式类型转换(比如int转double)。

  • 数组会被转换为指向元素类型的指针,函数转换为函数指针。

  • 支持派生类向基类的转换。这个在实际项目中非常实用,后面会展开。

如果外层没有匹配的catch,main函数最后通常会写一个catch(...)兜底,防止程序崩溃,但它只能捕获,没法知道具体错误信息。

2. 栈展开:异常的传播路径

栈展开(stack unwinding)是理解异常行为的关键。从throw开始,编译器会依次查找:

  1. 检查throw本身是否在try块内。如果在,看当前try块的catch有没有匹配的。

  2. 有匹配就跳到catch执行,之后从这个catch结束后的第一条语句继续运行。

  3. 没有匹配,则退出当前函数,析构所有局部对象,回到调用方函数的调用点,重复第一步。

  4. 到达main还没匹配,调用terminate终止程序。

假设调用链是:main() → func3() → func2() → func1()func1抛出异常,catch在main里。它会依次退出func1func2func3的栈帧,直到在main里找到匹配的catch。这里"退出"不是简单的跳转,而是沿着调用链,层层析构局部对象,这保证了RAII资源能被正确释放。

3. 自定义异常体系:利用派生类到基类的转换

在实际项目中,通常不会到处抛string或基本类型,而是构建一个异常类体系,基类可以是std::exception或者自己写的类,各模块派生自己的异常类型。捕获时只捕获基类引用,就能统一处理所有异常。

cpp

复制代码
// 基类
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) {}
    string what() const override {
        return "SqlException:" + _errmsg + "->" + _sql;
    }
private:
    string _sql;
};

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

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

业务函数模拟抛出各类异常:

cpp

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

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

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

主函数里只需要捕获基类引用:

cpp

复制代码
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 << "Unknown Exception" << endl;
        }
    }
}

捕获基类引用配合虚函数what(),既能拿到具体类型的信息,又不需要写一堆分支类型的catch。新增一个派生类异常,只要继承Exception并重写what(),上层代码一行不改。这就是开放-封闭原则在错误处理中的体现。

这里有一个细节:catch的参数应该用引用,否则会发生拷贝切片,派生类的额外信息就丢了。

小结

异常机制的本质是把错误检测和处理解耦,代价是引入了栈展开带来的控制流复杂性和资源管理风险。下一篇文章会顺着这个思路往下走:异常重新抛出、异常安全问题、以及C++11引入的noexcept规范------它们试图解决"异常本身带来的问题"。

相关推荐
小白学大数据8 小时前
深度探索:Python 爬虫实现豆瓣音乐全站采集
开发语言·爬虫·python·数据分析
Xin_ye100868 小时前
C# 零基础到精通教程 - 第八章:面向对象编程(进阶)——继承与多态
开发语言·c#
m0_748839498 小时前
R包grafify:简单操作实现高效统计绘图
开发语言·r语言
王老师青少年编程8 小时前
csp信奥赛C++高频考点专项训练之前缀和&差分 --【一维前缀和】:宝石串
c++·前缀和·csp·高频考点·信奥赛·宝石串
梓䈑8 小时前
【算法题攻略】模拟
c++·算法
Evand J8 小时前
【课题推荐与代码介绍】卡尔曼滤波器正反向估计算法原理与MATLAB实现
开发语言·算法·matlab
奋斗的小方8 小时前
Java基础篇09:项目实战
java·开发语言
Scott9999HH8 小时前
高端胶原蛋白饮多品牌深度对比:抗皱机理、国际研发标准、气色维度,一套可直接用的选品框架
安全
vKd0Ff21L8 小时前
如何在Dev-C++中设置TDM-GCC为默认编译器第九十一篇
java·jvm·c++