C++中的异常


文章目录


引言

在程序开发过程中,我们经常会遇到各种各样的错误,导致程序崩溃。C语言中通过错误码 处理错误,但错误码非常多,通常需要手动查询含义,效率低下;而C++中通过异常机制,将错误检测与处理分离,允许程序运行时传递更详细的错误信息,并灵活跳转至对应的处理逻辑,提升了代码的健壮性,效率更高。


1.异常的核心概念

异常时程序运行时出现的非正常情况(如逻辑错误、资源不足等),而C++的异常处理机制核心在于 "抛出异常"与"捕获异常" 的交互:

  1. 异常对象:是抛出异常的载体,可包含比错误码更丰富的信息(如错误描述、位置、类型等),支持自定义类型。
  2. 检测与处理分离:异常处理机制中,程序一部分负责检测错误并抛出异常 ,另一部分负责捕获并处理异常 ,这两部分是相互独立的。
  3. 优势:与C语言错误码相比,异常对象能传递更全面的错误信息 ,且无需在每层函数中手动返回错误码,进一步简化了代码逻辑。

2.异常的抛出与捕获

2.1.基本语法与流程

  1. 抛出异常:使用关键字throw来抛出异常对象(可以是内置类型、自定义类型或便准库类型),抛出异常后,当前函数中的后续代码将不再执行。
  2. 捕获异常:通过try-catch语句块实现,try包裹可能抛出异常的代码,catch指定要捕获的异常类型,匹配成功则执行处理逻辑。
  3. 核心规则:
    (1) 捕获 异常类型需要与抛出 异常类型相匹配(支持部分类型转换,如:非const转const,派生类转基类);
    (2) 若有多个匹配的catch块,优先执行离抛出点最近的那个;
    (3) 抛出的局部异常对象会生成拷贝,在catch处理后销毁(类似于函数的传值返回)。

2.2.栈展开

try-catch为捕获到匹配的异常时,程序会执行"栈展开"过程:

  1. 退出当前函数栈,并销毁该函中的所有局部变量;
  2. 向上层调用的函数栈帧继续查找匹配的catch块;
  3. 若一直追溯到main函数仍未找到,则调用terminate函数终止运行。

    代码示例:
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 除法函数:实现两整数除法,当除数为0时抛出string类型异常
double Divide(int a, int b)
{
    // 检测除数为0的错误场景
    if (b == 0)
    {
        // 创建异常信息字符串,包含具体错误原因
        string s("Divide by zero condition!");
        // 抛出string类型异常对象,后续代码不再执行
        throw s;
    }
    else
    {
        // 除数合法时,正常计算并返回除法结果(转换为double避免整数截断)
        return ((double)a / (double)b);
    }
}

// 功能函数:获取用户输入的两个整数,调用Divide函数计算并输出结果
void Func()
{
    int len, time;
    cin >> len >> time;

    // try块:包裹可能抛出异常的代码(此处为Divide函数调用)
    try
    {
        // 调用Divide函数计算结果并直接输出,若Divide抛出异常,此句后续代码不执行
        cout << Divide(len, time) << endl;
    }
    // catch块:捕获特定类型的异常(此处为const char*类型)
    // 注意:此catch块实际无法捕获Divide抛出的string类型异常,属于"类型不匹配"的设计
    catch (const char* errmsg)
    {
        // 若捕获到const char*类型异常,输出异常信息
        cout << errmsg << endl;
    }

    // 关键:无论try块中是否抛出异常、是否捕获成功,此句都会执行
    // __FUNCTION__是编译器内置宏,获取当前函数名(此处为"Func");__LINE__获取当前代码行号
    cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}

int main()
{
    // while(1):创建无限循环,让程序可重复接收用户输入(直到手动终止)
    while (1)
    {
        // try块:包裹可能抛出异常的函数调用(此处为Func函数)
        try
        {
            // 调用Func函数,若Func内部抛出未被捕获的异常,会传递到此处
            Func();
        }
        // catch块:捕获string类型异常(匹配Divide函数抛出的异常类型)
        catch (const string& errmsg)
        {
            // 输出捕获到的异常信息
            cout << errmsg << endl;
        }
    }

    return 0;
}

运行结果:

2.3.类型匹配的特殊情况

除了完全匹配,C++还支持三种特殊的类型转换匹配异常:

  1. 权限缩小:非常量类型可转换为const类型,如:throw string可被cathch(const string&)捕获;
  2. 数组/函数指针转换:数组退化为指向自身元素的指针,函数退化为指向其类型的指针;
  3. 继承体系转换:派生类异常,可被基类类型的catch捕获。
    代码示例:
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// ------------------------------
// 准备:定义继承体系(用于测试"继承体系转换")
// 基类异常
class BaseException {
public:
    // 虚函数支持多态,捕获基类引用时能调用派生类的实现
    virtual string what() const {
        return "BaseException: 基类异常";
    }
};

// 派生类1:SQL异常(继承自BaseException)
class SqlException : public BaseException {
public:
    string what() const override {
        return "SqlException: SQL语法错误";
    }
};

// 派生类2:网络异常(继承自BaseException)
class NetworkException : public BaseException {
public:
    string what() const override {
        return "NetworkException: 网络连接超时";
    }
};

// ------------------------------
// 测试1:权限缩小(非常量→const)
void test_const_conversion() {
    cout << "\n===== 测试1:权限缩小(非常量→const) =====" << endl;
    try {
        // 抛出:非常量string类型异常(无const修饰)
        throw string("抛出:非const字符串异常");
    }
    // 捕获:const string&(权限缩小,合法转换)
    catch (const string& e) {
        cout << "捕获成功:" << e << endl;
        cout << "说明:throw string(非const)可被catch(const string&)捕获" << endl;
    }
}

// ------------------------------
// 测试2:数组/函数指针转换(数组退化为指针、函数退化为指针)
// 辅助:测试函数(用于函数指针转换)
void test_func() {
    cout << "测试函数:用于函数指针转换" << endl;
}

void test_array_func_conversion() {
    cout << "\n===== 测试2:数组/函数指针转换 =====" << endl;

    // 子测试2.1:数组退化为指针
    try {
        int arr[5] = { 10, 20, 30, 40, 50 };
        // 抛出:数组类型(实际会退化为int*指针)
        throw arr;
    }
    // 捕获:int*指针(匹配数组退化后的类型,合法转换)
    catch (int* p) {
        cout << "子测试2.1(数组→指针):" << endl;
        cout << "捕获到数组退化的指针,首元素值:" << *p << endl;
        cout << "数组第3个元素值:" << *(p + 2) << endl; // 指针偏移访问
    }

    // 子测试2.2:函数退化为指针
    try {
        // 抛出:函数名(实际会退化为函数指针void(*)())
        throw test_func;
    }
    // 捕获:函数指针类型(匹配函数退化后的类型,合法转换)
    catch (void(*func_ptr)()) {
        cout << "\n子测试2.2(函数→指针):" << endl;
        cout << "捕获到函数指针,调用函数:";
        func_ptr(); // 通过捕获的函数指针调用原函数
    }
}

// ------------------------------
// 测试3:继承体系转换(派生类→基类)(实战最常用)
void test_inheritance_conversion() {
    cout << "\n===== 测试3:继承体系转换(派生类→基类) =====" << endl;

    // 子测试3.1:抛出SqlException(派生类),捕获BaseException(基类)
    try {
        throw SqlException(); // 抛出派生类异常
    }
    catch (const BaseException& e) { // 捕获基类引用
        cout << "子测试3.1:" << e.what() << endl;
        cout << "说明:SqlException(派生类)可被BaseException(基类)捕获" << endl;
    }

    // 子测试3.2:抛出NetworkException(派生类),捕获BaseException(基类)
    try {
        throw NetworkException(); // 抛出另一个派生类异常
    }
    catch (const BaseException& e) { // 统一捕获基类
        cout << "\n子测试3.2:" << e.what() << endl;
        cout << "说明:多个派生类异常可通过基类catch统一处理(实战核心用法)" << endl;
    }
}

// ------------------------------
// 主函数:执行所有测试
int main() {
    // 依次执行3种转换的测试
    test_const_conversion();
    test_array_func_conversion();
    test_inheritance_conversion();

    return 0;
}

运行结果:

2.4.异常重新抛出

有时catch捕获异常后,需对异常类型进行分类,其中某种特定类型异常需要进行特殊处理,其余异常交给上层处理,此时可使用可使用throw重新抛出异常。

典型案例:网络请求失败时,对"网络不稳定"异常重试3次,其余异常抛出给外层调用

cpp 复制代码
#include <iostream>
#include <string>
#include <ctime>  // 用于srand(time(0))初始化随机数
using namespace std;

// --------------------------
// 1. 补全选中片段隐含的异常类体系(核心依赖)
// 异常基类:所有自定义异常的父类
class Exception {
public:
    // 构造函数:初始化错误信息和错误码
    Exception(const string& errmsg, int id)
        : _errmsg(errmsg)
        , _id(id)
    {
    }

    // 虚函数what():返回错误描述(支持多态,派生类可重写)
    virtual string what() const {
        return _errmsg;
    }

    // 获取错误码:选中片段中e.getid()依赖此函数
    int getid() const {
        return _id;
    }

protected:
    string _errmsg;  // 错误描述信息
    int _id;         // 错误码(102=网络不稳定,103=非好友)
};

// 派生类:Http相关异常(选中片段中throw的异常类型)
class HttpException : public Exception {
public:
    // 构造函数:额外传入Http请求类型(如put/get)
    HttpException(const string& errmsg, int id, const string& type)
        : Exception(errmsg, id)  // 调用基类构造函数
        , _type(type)
    {
    }

    // 重写what():补充Http请求类型,让错误信息更详细
    virtual string what() const override {
        return "HttpException[" + _type + "]: " + _errmsg;
    }

private:
    string _type;  // 存储Http请求类型(如选中片段中的"put")
};

// --------------------------
// 2. 选中片段中的核心函数:_SeedMsg(实际发送消息,内部抛异常)
// 注意:函数名前的下划线通常表示"内部辅助函数",不建议外部直接调用
void _SeedMsg(const string& s) {
    // 随机模拟两种异常场景,或正常发送
    if (rand() % 2 == 0) {
        // 场景1:网络不稳定(错误码102,对应选中片段的重试逻辑)
        throw HttpException("网络不稳定,发送失败", 102, "put");
    }
    else if (rand() % 7 == 0) {
        // 场景2:非好友(错误码103,不重试,直接抛上层)
        throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
    }
    else {
        // 场景3:发送成功
        cout << "消息发送成功:" << s << endl;
    }
}

// 3. 选中片段中的核心函数:SendMsg(对外接口,包含重试逻辑)
void SendMsg(const string& s) {
    // 发送消息失败,则再重试3次(循环4次:0~3,共4次机会)
    for (size_t i = 0; i < 4; i++) {
        try {
            _SeedMsg(s);  // 调用内部发送函数
            break;        // 若发送成功,跳出循环,不再重试
        }
        // 捕获所有自定义异常(选中片段的核心catch逻辑)
        catch (const Exception& e) {
            // 分支1:错误码102(网络不稳定)→ 执行重试逻辑
            if (e.getid() == 102) {
                // 若重试到第3次(i=3)仍失败,不再重试,抛异常给上层
                if (i == 3) {
                    throw;  // 重新抛出当前捕获的异常(不修改异常内容)
                }
                // 未到最大重试次数,提示并继续循环重试
                cout << "开始第" << i + 1 << "次重试(原因:" << e.what() << ")" << endl;
            }
            // 分支2:非102错误码(如103非好友)→ 直接抛上层,不重试
            else {
                throw;  // 重新抛出异常,交给main函数的catch处理
            }
        }
    }
}

// --------------------------
// 4. 选中片段中的main函数(程序入口,处理最终异常)
int main() {
    srand(time(0));  // 初始化随机数种子(确保每次运行异常概率不同)
    string str;       // 存储用户输入的消息内容

    // 循环接收用户输入(输入一次,发送一次)
    while (cin >> str) {
        try {
            SendMsg(str);  // 调用发送函数,可能抛出异常
        }
        // 捕获所有自定义异常(最终异常处理)
        catch (const Exception& e) {
            cout << "最终处理异常:" << e.what() << endl << endl;
        }
        // 捕获未预期的其他异常(兜底处理,避免程序崩溃)
        catch (...) {
            cout << "Unkown Exception" << endl << endl;
        }
    }

    return 0;
}

运行结果:


3.异常安全与规范

3.1.异常安全问题

异常抛出后可能会导致资源泄露:

  • 资源申请后抛出异常 :如果在newdelete之间抛出异常,则会造成资源无法释放的问题;
  • 析构函数抛出异常:若析构函数抛出异常,可能会导致后续资源无法正常释放。

避免资源泄露的示例:

cpp 复制代码
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; // 正常执行时释放资源
}

我们还可以使用智能指针(RAII机制)来避免资源泄露,这个方法会在下一篇博客中讲到。

3.2.异常规范

  • C++98:函数参数列表后接throw,表示不抛异常 ;函数参数列表后接throw(类型1,类型2,...)表示可能会抛出多种类型的异常,各种类型之间用逗号分割;
  • C++11:关键字noexcept表示函数不会抛异常;不加则可能会抛异常。

注意:

(1)若标记noexcept的函数抛出异常,程序会调用terminate终止;

(2)noexcept(表达式)可作为运算符 ,判断表达式是否可能抛出异常 ,返回值为true/false


代码示例:

cpp 复制代码
// C++98风格
void* operator new(std::size_t size) throw(std::bad_alloc); // 仅抛bad_alloc
void* operator delete(std::size_t size, void* ptr) throw(); // 不抛异常

// C++11风格
double Divide(int a, int b) noexcept {
  if (b == 0) throw "Division by zero!"; // 编译通过,但运行时抛异常会终止程序
  return (double)a / b;
}

// noexcept作为运算符
cout << noexcept(Divide(1,2)) << endl; // true(表达式本身无异常)
cout << noexcept(Divide(1,0)) << endl; // true(编译器不分析运行时逻辑)

4.标准库异常体系

  • C++标准库定义了一套自己的异常继承体系库,基类是exception,可以直接在主函数捕获它,要获取异常信息,可以调用what函数(一个虚函数,派生类可以重写)。
  • 点击查看异常继承体系库中的详细内容

结语

C++异常处理通过 "抛出-捕获"机制 ,让错误检测与处理清晰分离。它摆脱了错误码的层层传递冗余**,借栈展开保障资源安全,凭继承体系实现统一适配。简洁的语法与灵活的逻辑跳转,既提升了代码稳健性,也让业务逻辑更纯粹,帮助我们快速找到程序中的错误,是C++构建可靠程序的核心工具之一。

相关推荐
MC丶科3 小时前
Java设计模式漫画英雄宇宙-工厂模式 —Factory博士的“超级英雄制造机”!
java·设计模式·漫画
Jerry3 小时前
问题记录 - Android IdleHandler 没有执行
android
虎子_layor3 小时前
告别Redis瓶颈:Caffeine本地缓存优化实战指南
java·后端
q***98523 小时前
什么是Spring Boot 应用开发?
java·spring boot·后端
带刺的坐椅3 小时前
Solon AI 开发学习4 - chat - 模型实例的构建和简单调用
java·ai·chatgpt·solon
hadage2333 小时前
--- JavaScript 的一些常用语法总结 ---
java·前端·javascript
没有了遇见3 小时前
Android ButterKnife Android 35情况下 适配 Gradle 8.+
android
合作小小程序员小小店3 小时前
桌面安全开发,桌面二进制%恶意行为拦截查杀%系统安全开发3.0,基于c/c++语言,mfc,win32,ring3,dll,hook,inject,无数据库
c语言·开发语言·c++·安全·系统安全
Codeking__3 小时前
C++ 11 atomic 原子性操作
开发语言·c++