【C++篇】让错误被温柔对待(上):异常基础与核心机制

文章目录

    • C++异常机制详解(一):异常基础与核心机制
    • 一、异常的概念与起源
      • [1.1 什么是异常](#1.1 什么是异常)
      • [1.2 C语言的错误处理方式](#1.2 C语言的错误处理方式)
      • [1.3 C++异常的优势](#1.3 C++异常的优势)
    • 二、异常的抛出与捕获
      • [2.1 抛出异常:throw](#2.1 抛出异常:throw)
      • [2.2 捕获异常:try-catch](#2.2 捕获异常:try-catch)
      • [2.3 异常对象的生命周期](#2.3 异常对象的生命周期)
    • 三、栈展开机制
      • [3.1 什么是栈展开](#3.1 什么是栈展开)
      • [3.2 栈展开过程演示](#3.2 栈展开过程演示)
      • [3.3 栈展开与对象析构](#3.3 栈展开与对象析构)
    • 四、异常的匹配规则
      • [4.1 精确匹配](#4.1 精确匹配)
      • [4.2 允许的类型转换](#4.2 允许的类型转换)
      • [4.3 多个catch的匹配顺序](#4.3 多个catch的匹配顺序)
      • [4.4 catch(...) 万能捕获](#4.4 catch(...) 万能捕获)
    • 五、实际应用:设计异常继承体系
      • [5.1 为什么需要异常继承体系](#5.1 为什么需要异常继承体系)
      • [5.2 设计服务器异常体系](#5.2 设计服务器异常体系)
      • [5.3 各模块的实现](#5.3 各模块的实现)
      • [5.4 统一捕获处理](#5.4 统一捕获处理)
    • 六、总结与展望
      • [6.1 本文要点回顾](#6.1 本文要点回顾)
      • [6.2 下一篇预告](#6.2 下一篇预告)

C++异常机制详解(一):异常基础与核心机制

💬 欢迎讨论:异常处理是C++中重要的错误处理机制,它让程序能够优雅地处理运行时错误。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!

👍 点赞、收藏与分享:这是C++异常机制系列的第一篇,建议收藏后系统学习。如果觉得有帮助,请分享给更多的朋友!

🚀 系列导航:本文将介绍异常的基本概念、抛出与捕获机制、栈展开过程以及实际应用案例。


一、异常的概念与起源

1.1 什么是异常

在程序运行过程中,我们经常会遇到各种错误情况:

  • 除零错误
  • 内存分配失败
  • 文件打开失败
  • 网络连接中断
  • 数组越界访问

这些错误如果不加以处理,程序可能会崩溃或产生未定义的行为。异常处理机制就是C++提供的一种结构化的错误处理方式。

异常处理的核心思想

异常机制允许我们将问题的检测问题的处理分离开来:

  • 检测部分:发现问题时抛出异常
  • 处理部分:在调用链的适当位置捕获并处理异常
  • 分离优势:检测代码无需知道处理细节,处理代码也无需知道检测细节

1.2 C语言的错误处理方式

在学习C++异常之前,我们先回顾一下C语言是如何处理错误的。

错误码方式

C语言主要通过返回错误码来处理错误:

cpp 复制代码
// C语言风格的错误处理
int divide(int a, int b, double* result)
{
    if (b == 0)
    {
        return -1;  // 错误码:除零错误
    }
    
    *result = (double)a / b;
    return 0;  // 成功
}

int main()
{
    double result;
    int ret = divide(10, 0, &result);
    
    if (ret == -1)
    {
        printf("除零错误\n");
    }
    else
    {
        printf("结果: %f\n", result);
    }
    
    return 0;
}

C语言错误处理的问题

  1. 错误码语义不明确 :看到-1需要查文档才知道是什么错误
  2. 容易被忽略:调用者可能忘记检查返回值
  3. 传递困难:错误码需要层层返回,代码冗长
  4. 信息有限:只能返回一个整数,无法携带详细信息
  5. 混淆返回值:正常返回值和错误码混在一起
cpp 复制代码
// 多层调用时的麻烦
int funcC()
{
    // ...
    if (error)
        return -1;
    return 0;
}

int funcB()
{
    int ret = funcC();
    if (ret != 0)
        return ret;  // 层层传递错误码
    return 0;
}

int funcA()
{
    int ret = funcB();
    if (ret != 0)
        return ret;  // 继续传递
    return 0;
}

1.3 C++异常的优势

C++的异常机制解决了C语言错误处理的诸多问题:

优势一:信息丰富

异常抛出的是一个对象,可以包含丰富的错误信息:

cpp 复制代码
class DivideException
{
public:
    DivideException(const string& msg, int line)
        : _msg(msg)
        , _line(line)
    {}
    
    string what() const { return _msg; }
    int line() const { return _line; }
    
private:
    string _msg;
    int _line;
};

优势二:自动传播

异常会自动沿着调用链向上传播,无需手动层层返回:

cpp 复制代码
void funcC()
{
    throw runtime_error("error in funcC");
}

void funcB()
{
    funcC();  // 不需要检查返回值,异常会自动向上传播
}

void funcA()
{
    try
    {
        funcB();
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;  // 在这里统一处理
    }
}

优势三:强制处理

未处理的异常会导致程序终止,这迫使程序员必须考虑错误处理。

优势四:不影响正常逻辑

正常的业务逻辑和错误处理逻辑分离,代码更清晰:

cpp 复制代码
// 正常逻辑清晰
try
{
    step1();
    step2();
    step3();
}
catch (...)  // 错误处理集中在这里
{
    handleError();
}

二、异常的抛出与捕获

2.1 抛出异常:throw

使用throw关键字抛出异常对象:

基本语法

cpp 复制代码
throw 异常对象;

抛出不同类型的异常

cpp 复制代码
// 抛出整数
void func1()
{
    throw 404;
}

// 抛出字符串
void func2()
{
    throw "File not found";
}

// 抛出自定义对象
void func3()
{
    throw DivideException("除零错误", 42);
}

// 抛出标准库异常
void func4()
{
    throw runtime_error("运行时错误");
}

throw的执行语义

throw执行时会发生什么:

  1. 立即终止当前函数:throw后面的语句不再执行
  2. 创建异常对象的副本:即使抛出的是局部对象,也会创建一个副本
  3. 开始栈展开:沿着调用链向上查找匹配的catch
cpp 复制代码
void testThrow()
{
    cout << "throw之前" << endl;
    throw 1;
    cout << "throw之后" << endl;  // 这行不会执行
}

int main()
{
    try
    {
        testThrow();
    }
    catch (int e)
    {
        cout << "捕获异常: " << e << endl;
    }
    
    return 0;
}

输出

bash 复制代码
throw之前
捕获异常: 1

2.2 捕获异常:try-catch

使用try-catch块来捕获和处理异常。

基本语法

cpp 复制代码
try
{
    // 可能抛出异常的代码
}
catch (异常类型1 参数)
{
    // 处理类型1的异常
}
catch (异常类型2 参数)
{
    // 处理类型2的异常
}
catch (...)
{
    // 捕获所有类型的异常
}

完整示例

cpp 复制代码
double Divide(int a, int b)
{
    if (b == 0)
    {
        throw "Division by zero!";
    }
    return (double)a / b;
}

int main()
{
    try
    {
        cout << Divide(10, 2) << endl;  // 正常执行
        cout << Divide(10, 0) << endl;  // 抛出异常
        cout << "这行不会执行" << endl;
    }
    catch (const char* msg)
    {
        cout << "捕获异常: " << msg << endl;
    }
    
    cout << "程序继续执行" << endl;
    return 0;
}

输出

bash 复制代码
5
捕获异常: Division by zero!
程序继续执行

2.3 异常对象的生命周期

这是一个重要但容易被忽视的细节。

异常对象的拷贝

抛出异常时,会创建异常对象的一个副本:

cpp 复制代码
class MyException
{
public:
    MyException(const string& msg) : _msg(msg)
    {
        cout << "MyException构造" << endl;
    }
    
    MyException(const MyException& e) : _msg(e._msg)
    {
        cout << "MyException拷贝构造" << endl;
    }
    
    ~MyException()
    {
        cout << "MyException析构" << endl;
    }
    
    string what() const { return _msg; }
    
private:
    string _msg;
};

void func()
{
    MyException e("test error");
    throw e;
}

int main()
{
    try
    {
        func();
    }
    catch (const MyException& ex)
    {
        cout << "捕获: " << ex.what() << endl;
    }
    
    return 0;
}

输出分析

bash 复制代码
MyException构造
MyException拷贝构造
MyException析构
捕获: test error
MyException析构

可以看到:

  1. 在func中构造了原始异常对象
  2. throw时进行了拷贝构造(创建副本)
  3. 原始对象在func结束时析构
  4. catch捕获的是副本
  5. catch块结束后副本析构

为什么要拷贝?

因为抛出的异常对象可能是局部对象,当函数退出时局部对象会被销毁。为了让外层能够捕获到异常信息,必须创建一个副本。


三、栈展开机制

3.1 什么是栈展开

当异常被抛出后,程序会执行一个叫做栈展开(Stack Unwinding)的过程。

栈展开的步骤

  1. 首先检查throw本身是否在try块内
  2. 如果在,查找匹配的catch子句
  3. 如果找到匹配的catch,跳转到catch块执行
  4. 如果没找到,退出当前函数,在上层调用函数中继续查找
  5. 重复步骤2-4,直到找到匹配的catch或到达main函数
  6. 如果到达main还没找到,调用terminate()终止程序

3.2 栈展开过程演示

cpp 复制代码
void funcD()
{
    cout << "funcD() 开始" << endl;
    throw runtime_error("error in funcD");
    cout << "funcD() 结束" << endl;  // 不会执行
}

void funcC()
{
    cout << "funcC() 开始" << endl;
    funcD();
    cout << "funcC() 结束" << endl;  // 不会执行
}

void funcB()
{
    cout << "funcB() 开始" << endl;
    try
    {
        funcC();
    }
    catch (const logic_error& e)  // 类型不匹配
    {
        cout << "funcB捕获logic_error" << endl;
    }
    cout << "funcB() 结束" << endl;  // 不会执行
}

void funcA()
{
    cout << "funcA() 开始" << endl;
    try
    {
        funcB();
    }
    catch (const runtime_error& e)  // 类型匹配!
    {
        cout << "funcA捕获: " << e.what() << endl;
    }
    cout << "funcA() 结束" << endl;  // 会执行
}

int main()
{
    funcA();
    cout << "main结束" << endl;
    return 0;
}

输出

bash 复制代码
funcA() 开始
funcB() 开始
funcC() 开始
funcD() 开始
funcA捕获: error in funcD
funcA() 结束
main结束

栈展开路径分析

  1. funcD抛出异常,检查自己没有try-catch
  2. 退出funcD,在funcC中查找(funcC也没有try-catch)
  3. 退出funcC,在funcB中查找(funcB有try-catch但类型不匹配)
  4. 退出funcB,在funcA中查找(funcA有匹配的catch!)
  5. 跳转到funcA的catch块执行
  6. catch块执行完,funcA继续执行后续代码

3.3 栈展开与对象析构

栈展开过程中,所有已构造的局部对象都会被正确析构:

cpp 复制代码
class Resource
{
public:
    Resource(const string& name) : _name(name)
    {
        cout << _name << " 构造" << endl;
    }
    
    ~Resource()
    {
        cout << _name << " 析构" << endl;
    }
    
private:
    string _name;
};

void funcInner()
{
    Resource r3("r3");
    throw runtime_error("error");
    Resource r4("r4");  // 不会构造
}

void funcOuter()
{
    Resource r1("r1");
    Resource r2("r2");
    funcInner();
    Resource r5("r5");  // 不会构造
}

int main()
{
    try
    {
        funcOuter();
    }
    catch (const exception& e)
    {
        cout << "捕获: " << e.what() << endl;
    }
    
    return 0;
}

输出

bash 复制代码
r1 构造
r2 构造
r3 构造
r3 析构
r2 析构
r1 析构
捕获: error

重要观察

  1. 抛出异常前构造的对象(r1, r2, r3)都被正确析构
  2. 抛出异常后的对象(r4, r5)不会被构造
  3. 析构顺序与构造顺序相反(栈的特性)

这个特性非常重要,它保证了即使发生异常,资源也能被正确释放。


四、异常的匹配规则

4.1 精确匹配

异常类型必须与catch的参数类型匹配:

cpp 复制代码
try
{
    throw 10;
}
catch (int e)     // 匹配
{
    cout << "捕获int: " << e << endl;
}
catch (double e)  // 不匹配
{
    cout << "捕获double" << endl;
}

4.2 允许的类型转换

虽然要求类型匹配,但C++允许以下几种类型转换:

转换1:非const向const转换(权限缩小)

cpp 复制代码
try
{
    throw 10;
}
catch (const int& e)  // OK,int可以转换为const int
{
    cout << e << endl;
}

转换2:数组/函数转指针

cpp 复制代码
try
{
    throw "error";  // const char[6]
}
catch (const char* msg)  // OK,数组转指针
{
    cout << msg << endl;
}

转换3:派生类向基类转换(重要!)

cpp 复制代码
class Base {};
class Derived : public Base {};

try
{
    throw Derived();
}
catch (const Base& e)  // OK,派生类可以转换为基类
{
    cout << "捕获Base" << endl;
}

这个特性非常重要,我们在设计异常体系时会大量使用。

4.3 多个catch的匹配顺序

当有多个catch时,按照从上到下的顺序匹配,第一个匹配的catch会被执行

cpp 复制代码
try
{
    throw 10;
}
catch (double e)
{
    cout << "捕获double" << endl;
}
catch (int e)
{
    cout << "捕获int" << endl;
}
catch (...)
{
    cout << "捕获所有" << endl;
}

输出捕获int

重要原则:将更具体的异常类型放在前面,更一般的类型放在后面。

cpp 复制代码
class Base {};
class Derived : public Base {};

try
{
    throw Derived();
}
catch (const Derived& e)  // 先捕获派生类
{
    cout << "捕获Derived" << endl;
}
catch (const Base& e)     // 再捕获基类
{
    cout << "捕获Base" << endl;
}

4.4 catch(...) 万能捕获

catch(...)可以捕获任意类型的异常:

cpp 复制代码
try
{
    // 可能抛出各种异常
}
catch (...)
{
    cout << "捕获到未知异常" << endl;
}

使用场景

  1. main函数的最后防线:确保程序不会因未捕获异常而崩溃
cpp 复制代码
int main()
{
    try
    {
        // 程序主逻辑
    }
    catch (const exception& e)
    {
        cout << "标准异常: " << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常,程序即将退出" << endl;
    }
    
    return 0;
}
  1. 析构函数中:确保析构过程不抛出异常
cpp 复制代码
~MyClass()
{
    try
    {
        // 清理资源
    }
    catch (...)
    {
        // 吞掉所有异常,不让异常逃离析构函数
    }
}

五、实际应用:设计异常继承体系

5.1 为什么需要异常继承体系

在大型项目中,不同的模块会抛出不同类型的异常。如果每个模块都定义自己独立的异常类型,main函数就需要catch很多种类型,非常不便。

通过继承体系,我们可以:

  1. 统一在基类捕获所有异常
  2. 每个模块添加自己特有的信息
  3. 保持代码的扩展性和维护性

5.2 设计服务器异常体系

假设我们要实现一个Web服务器,包含HTTP服务、SQL数据库、缓存等模块。

基类设计

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;
};

SQL模块异常

cpp 复制代码
class SqlException : public Exception
{
public:
    SqlException(const string& errmsg, int id, const string& sql)
        : Exception(errmsg, id)
        , _sql(sql)
    {}
    
    virtual string what() const override
    {
        string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;
        return str;
    }
    
private:
    string _sql;  // SQL语句
};

缓存模块异常

cpp 复制代码
class CacheException : public Exception
{
public:
    CacheException(const string& errmsg, int id)
        : Exception(errmsg, id)
    {}
    
    virtual string what() const override
    {
        string str = "CacheException:";
        str += _errmsg;
        return str;
    }
};

HTTP模块异常

cpp 复制代码
class HttpException : public Exception
{
public:
    HttpException(const string& errmsg, int id, const string& type)
        : Exception(errmsg, id)
        , _type(type)
    {}
    
    virtual string what() const override
    {
        string str = "HttpException:";
        str += _type;
        str += ":";
        str += _errmsg;
        return str;
    }
    
private:
    string _type;  // HTTP方法类型:GET/POST等
};

5.3 各模块的实现

cpp 复制代码
#include <thread>
#include <chrono>

// SQL管理模块
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();
}

// HTTP服务器模块
void HttpServer()
{
    if (rand() % 3 == 0)
    {
        throw HttpException("请求资源不存在", 100, "GET");
    }
    else if (rand() % 4 == 0)
    {
        throw HttpException("权限不足", 101, "POST");
    }
    
    cout << "HttpServer 调用成功" << endl;
    CacheMgr();
}

5.4 统一捕获处理

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;
            cout << "错误ID: " << e.getid() << endl;
        }
        catch (...)
        {
            cout << "Unknown Exception" << endl;
        }
    }
    
    return 0;
}

运行效果

bash 复制代码
HttpServer 调用成功
CacheMgr 调用成功
SQLMgr 调用成功
HttpException:GET:请求资源不存在
错误ID: 100
HttpServer 调用成功
CacheException:数据不存在
错误ID: 101
SqlException:权限不足->select * from name = '张三'
错误ID: 100
...

设计优势

  1. 统一接口:main函数只需捕获Exception基类
  2. 扩展性强:添加新模块只需继承Exception
  3. 信息完整:每个模块可以添加自己特有的信息
  4. 多态机制:通过虚函数what()实现不同的错误描述

六、总结与展望

6.1 本文要点回顾

异常的概念

  • 异常是C++提供的结构化错误处理机制
  • 相比C语言的错误码,异常能携带更丰富的信息
  • 异常将问题检测和处理分离,代码更清晰

异常的抛出与捕获

  • 使用throw抛出异常对象
  • 使用try-catch捕获并处理异常
  • 抛出异常时会创建对象的副本

栈展开机制

  • 异常沿着调用链向上查找匹配的catch
  • 展开过程中,已构造的对象会被正确析构
  • 未捕获的异常会导致程序终止

异常匹配规则

  • 要求类型精确匹配(允许少数转换)
  • 派生类可以转换为基类(重要特性)
  • 多个catch按顺序匹配,第一个匹配的被执行
  • catch(...)可以捕获任意类型

实际应用

  • 通过继承设计异常体系
  • 基类统一接口,派生类添加特有信息
  • 实现灵活的错误分类和处理

6.2 下一篇预告

在下一篇文章中,我们将学习异常的高级特性:

  • 异常重新抛出:捕获后重新抛出的技巧
  • 异常安全:如何避免异常导致的资源泄漏
  • 异常规范:noexcept的使用
  • 标准库异常:C++标准库的异常体系
  • 最佳实践:异常使用的注意事项

通过本文的学习,我们掌握了C++异常机制的基础知识和核心概念。异常是现代C++中不可或缺的特性,正确使用异常能让程序更加健壮和易于维护!

以上就是C++异常机制基础的全部内容,期待在下一篇文章中与你继续探讨异常的高级特性!如有疑问,欢迎在评论区交流讨论!❤️

相关推荐
沐知全栈开发2 小时前
R 语言中的判断语句
开发语言
戴西软件2 小时前
CAxWorks.VPG车辆工程仿真软件:打造新能源汽车安全的“数字防线“
android·大数据·运维·人工智能·安全·低代码·汽车
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之mlabel命令(实操篇)
linux·运维·服务器·前端·笔记
Victor3562 小时前
Hibernate(8)什么是Hibernate的SessionFactory?
后端
Victor3562 小时前
Hibernate(7)Hibernate的Session是什么?
后端
zd8451015002 小时前
[LWIP] LWIP热插拔功能 问题调试
开发语言·php
2501_925866162 小时前
Docker搭建HomeAssistant平台
运维·docker·容器
趁月色小酌***2 小时前
JAVA 知识点总结4
java·开发语言
独断万古他化2 小时前
【Spring Web MVC 入门续篇】请求处理之 Cookie 与 Session 获取实战
后端·spring·java-ee·mvc