跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

深入理解 C++ 异常机制

C++ 异常机制通过 throw 和 catch 实现错误处理,核心思想是检测与解决分离。异常抛出后沿调用链传播(栈展开),直到匹配 catch 或终止程序。支持严格匹配、基类转换及通配符捕获。异常安全需依赖 RAII 和智能指针防止资源泄漏。C++11 引入 noexcept 规范替代旧版 throw(),确保函数不抛异常时提升性能与稳定性。掌握这些机制有助于编写健壮代码。

疯疯癫癫发布于 2026/2/5更新于 2026/6/2740 浏览
深入理解 C++ 异常机制

前言

在 C++ 编程中,异常处理是一种强大的机制,能够帮助程序在运行时优雅地处理错误,避免崩溃,提高程序的健壮性和可维护性。相比 C 语言通过错误码进行错误处理的方式,C++ 的异常处理提供了更清晰、更结构化的错误处理方法。

1. 异常的概念

在 C++ 中,异常(Exception)是一种特殊的事件,当程序运行过程中发生错误时,程序可以抛出异常,并由相应的异常处理代码进行处理。

异常处理的核心思想

异常处理的核心思想是将问题的检测与问题的解决分开:

  • 检测错误的代码 负责发现问题,并抛出异常。
  • 异常处理代码 负责捕获异常并执行相应的错误处理逻辑。
C 语言与 C++ 异常的区别

在 C 语言中,通常使用 错误码 进行错误处理:

  • 需要手动检查函数的返回值,以判断是否出现错误。
  • 错误码需要进行分类编号,程序需要额外的错误查询机制,处理逻辑复杂。

C++ 通过异常对象进行错误处理:

  • 直接抛出一个异常对象,其中可以包含完整的错误信息。
  • 由异常处理机制自动捕获异常,避免手动检查返回值,使代码更清晰。

2. 异常的抛出与捕获

当程序检测到错误时,可以使用 throw 关键字抛出异常对象,并使用 catch 语句捕获异常进行处理。

异常的处理流程
  1. 发生错误时,程序会 throw 一个异常对象。
  2. 异常会沿着调用链向上传播,直到找到匹配的 catch 语句。
  3. 如果找到匹配的 catch,则执行其中的异常处理代码。
  4. 如果没有找到匹配的 catch,则程序会终止。
抛出异常的规则
  • throw 语句后面的代码不会被执行,程序会立即跳转到匹配的 catch 语句。
  • 异常对象会被复制一份,并在 catch 语句处理后销毁。
  • catch 语句必须与异常的类型匹配,否则异常会继续向上传播。
匹配规则
  • 严格匹配:catch 语句的类型必须与 throw 语句的类型一致,或者是它的基类。
  • 基类匹配:如果 catch 语句的参数是基类对象,可以捕获派生类异常(多态)。
  • 通配符捕获:使用 catch (...) 语句,可以捕获所有类型的异常。
#include <iostream>
#include <vector>
#include <string>
using namespace std;

{
     {
         (b == ) {
            string error = ;
             error;
        }   ()a / b;
    }  ( errid) {
        cout << errid << endl;
    }
     ;
}

{
     len, time;
    cin >> len >> time;
     {
        cout << (len, time) << endl;
    }  ( * errmsg) {
        cout << errmsg << endl;
    }
    cout << __FUNCTION__ <<  << __LINE__ <<  << endl;
}

{
     () {
         {
            ();
        }  ( string& errmsg) {
            cout << errmsg << endl;
        }
    }
     ;
}
double divide(int a, int b)
try
if
0
"Divide by zero"
throw
else
return
double
catch
int
return
0
void Func()
int
try
divide
catch
const
char
":"
"行执行"
int main()
while
true
try
Func
catch
const
return
0

下面就是调用最近的 Func 函数的 catch 语句示例:

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

double divide(int a, int b) {
    if (b == 0) {
        throw string("Divide by zero"); // 直接抛出 std::string
    }
    return (double)a / b;
}

void Func() {
    int len, time;
    cin >> len >> time;
    try {
        cout << divide(len, time) << endl;
    } catch (const string& errmsg) {
        cout << "Error in Func: " << errmsg << endl;
    }
    cout << __FUNCTION__ << ":" << __LINE__ << " 行执行" << endl;
}

int main() {
    while (true) {
        try {
            Func();
        } catch (const string& errmsg) {
            cout << "Error in main: " << errmsg << endl;
        }
    }
    return 0;
}

3. 栈展开

栈展开(Stack Unwinding)指的是当异常发生时,程序会沿着函数调用栈回溯,查找匹配的 catch 语句的过程。

栈展开的过程
  1. 检查当前函数 是否有匹配的 catch 语句:
    • 如果有,跳转到 catch 语句处理异常。
    • 如果没有,退出当前函数,并在上层调用链中继续查找。
  2. 清理局部变量:
    • 退出函数时,会调用当前作用域内的局部对象的析构函数,释放资源。
  3. 继续向上查找 catch:
    • 如果找到了匹配的 catch,程序继续执行 catch 语句中的代码。
    • 如果一直找不到 catch,最终会到达 main 函数。
  4. 程序终止:
    • 如果 main 函数也没有捕获异常,程序会调用 std::terminate() 终止执行。

4. 异常的匹配规则

当异常被抛出时,程序会寻找匹配的 catch 语句进行处理,匹配规则如下:

  • 完全匹配:抛出的异常类型和 catch 语句的参数类型完全一致。
  • 常量转换:允许从非常量向常量转换(如 int → const int)。
  • 数组和指针转换:允许数组转换成指向数组元素类型的指针。
  • 派生类向基类转换:允许派生类异常被基类的 catch 语句捕获(多态)。
  • 通配符匹配:catch (...) 可捕获所有异常,但无法识别异常类型。
#include <thread>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <string>
using namespace std;

// ⼀般⼤型项⽬程序才会使⽤异常,下⾯我们模拟设计⼀个服务的⼏个模块
// 每个模块的继承都是 Exception 的派⽣类,每个模块可以添加⾃⼰的数据
// 最后捕获时,我们捕获基类就可以
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 {
        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 (true) {
        this_thread::sleep_for(chrono::seconds(1));
        try {
            HttpServer();
        } catch (const Exception& e) // 这⾥捕获基类,基类对象和派⽣类对象都可以被捕获
        {
            cout << e.what() << endl;
        } catch (...) {
            cout << "Unknown Exception" << endl;
        }
    }
    return 0;
}

5. 异常的重新抛出

在某些情况下,一个 catch 语句可能需要部分处理异常,然后重新抛出它,让更外层的 catch 处理剩余部分。可以使用 throw; 重新抛出当前异常。

典型应用
  • 先记录错误日志,然后重新抛出异常,让外层代码进行更详细的处理。
#include <iostream>
#include <stdexcept>
using namespace std;

void func() {
    int* arr = new int[10]; // 申请内存
    try {
        throw runtime_error("Memory error"); // 模拟异常
    } catch (const exception& e) {
        cout << "Error in func: " << e.what() << endl;
        delete[] arr; // 释放资源
        throw; // 重新抛出异常
    }
}

int main() {
    try {
        func();
    } catch (const exception& e) {
        cout << "Main caught: " << e.what() << endl;
    }
    return 0;
}

6. 异常安全问题

异常处理的一个关键问题是如何确保资源不会因为异常而泄漏,例如:

  • 动态内存分配:如果在 new 之后异常被抛出,delete 可能不会被执行,导致内存泄漏。
  • 锁管理:如果在获取锁后抛出异常,而没有释放锁,则可能导致死锁。
  • 文件操作:打开文件后,异常可能导致文件未能正确关闭。
解决方案
  1. RAII(资源获取即初始化)
    • 资源在构造函数中分配,在析构函数中释放,确保异常安全。
    • 例如使用 智能指针(std::unique_ptr 和 std::shared_ptr) 来管理内存。
  2. try-catch 处理资源释放
    • 在 catch 语句中手动释放资源,然后重新抛出异常。
  3. 避免异常逃离析构函数
    • 析构函数不应抛出异常,否则会导致程序 std::terminate() 终止。
#include <iostream>
#include <string>
using namespace std;

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

void Func() {
    // 这里可以看到如果发⽣除 0 错误抛出异常,另外下面的 array 没有得到释放。
    // 所以这里捕获异常后并不处理异常,异常还是交给外层处理,这里捕获了再重新抛出去。
    int* array = new int[10];
    try {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    } catch (...) {
        // 捕获异常释放内存
        cout << "delete []" << array << endl;
        delete[] array;
        throw; // 异常重新抛出,捕获到什么抛出什么
    }
    cout << "delete []" << array << endl;
    delete[] array;
}

int main() {
    try {
        Func();
    } catch (const char* errmsg) {
        cout << errmsg << endl;
    } catch (const exception& e) {
        cout << e.what() << endl;
    } catch (...) {
        cout << "Unknown Exception" << endl;
    }
    return 0;
}

7. 异常规范

C++ 提供了一些异常规范,用于声明函数是否会抛出异常。

C++98 的异常规范
  • throw():表示函数不会抛出任何异常。
  • throw(int, char, std::exception):表示函数可能抛出指定类型的异常。

⚠ 问题:C++98 的异常规范很复杂,难以维护,并且编译器不会强制检查。

C++11 的 noexcept
  • 如果 noexcept 修饰的函数抛出异常,程序会终止!

noexcept(expression):可以检测表达式是否可能抛出异常:

bool canThrow = noexcept(func());

noexcept 关键字替代了 throw(),表示函数不会抛出异常:

void func() noexcept;
实践建议
  • 仅在确保函数不会抛出异常时使用 noexcept,否则可能导致程序异常终止。

总结

C++ 的异常处理机制提供了一种清晰、结构化的错误处理方式,避免了 C 语言繁琐的错误码处理。关键点如下:

  1. throw 抛出异常,catch 捕获异常,异常对象可以携带详细错误信息。
  2. 异常沿着调用链传播(栈展开),直到找到匹配的 catch,否则程序终止。
  3. 异常安全 是编写健壮代码的重要原则,应使用 RAII、智能指针 等机制避免资源泄漏。
  4. C++11 noexcept 规范化了异常处理,提高了代码的可维护性和优化能力。

掌握 C++ 异常处理,将有助于编写更加稳定和健壮的程序!

目录

  1. 前言
  2. 1. 异常的概念
  3. 异常处理的核心思想
  4. C 语言与 C++ 异常的区别
  5. 2. 异常的抛出与捕获
  6. 异常的处理流程
  7. 抛出异常的规则
  8. 匹配规则
  9. 3. 栈展开
  10. 栈展开的过程
  11. 4. 异常的匹配规则
  12. 5. 异常的重新抛出
  13. 典型应用
  14. 6. 异常安全问题
  15. 解决方案
  16. 7. 异常规范
  17. C++98 的异常规范
  18. C++11 的 noexcept
  19. 实践建议
  20. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • DeepSeek 时代:前端开发者的护城河与转型方向
  • DeepSeek-R1-Distill-Llama-8B 优化实战:提升文本生成质量
  • IDE 调用大模型 Session 机制解析与实践指南
  • Python 面向对象编程与异常处理指南
  • Git 与 TortoiseGit 小乌龟安装配置指南
  • Python 7 个网络爬虫实战案例及源码解析
  • 使用 ssprompt 工具生成小红书爆款文案
  • Visual Studio 2022 安装指南(C++ 桌面开发)
  • 轻小说机翻机器人:架构设计与快速部署
  • 前端监控实战:错误、性能与用户行为实时监测
  • 前端三剑客:HTML、CSS、JavaScript 关系详解
  • Python 爬虫学习的四大阶段与核心技能
  • Whisper 模型参数调优实战:适配不同语音识别场景
  • 连通块问题解析与 C++ 代码实现
  • C/C++ 内存管理与动态分配详解
  • 自然语言处理在医疗领域的应用与实战
  • 深入理解数据结构中的时间与空间复杂度
  • 商汤开源 SenseNova-MARS 多模态自主推理模型
  • .NET 集成 GoView 低代码可视化大屏实战指南
  • 二分查找算法实战:插入位置、平方根与峰顶索引

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online