C++异常处理指南:构建健壮程序的错误处理机制

发布于:2025-08-30 ⋅ 阅读:(14) ⋅ 点赞:(0)

在程序开发的世界里,“错误” 是绕不开的话题。你可能写过一个简单的计算器,却因为用户输入 “5÷0” 而崩溃;也可能在操作数据库时,因为权限不足导致数据读取失败;甚至在申请内存时,因为系统资源耗尽而无法继续执行。这些运行时出现的意外情况,若不妥善处理,轻则导致程序异常退出,重则引发数据丢失、资源泄漏等严重问题。

C 语言中,我们通常用 “错误码” 来应对这类问题 —— 比如用-1表示函数执行失败,用全局变量errno存储错误类型。但这种方式的缺陷显而易见:错误码只能传递简单的整数信息,想要知道具体错误原因(比如 “为什么打开文件失败?是文件不存在还是权限不够?”),还得手动查询错误码对照表;更麻烦的是,错误码需要函数调用者主动检查,一旦遗漏,错误就会 “沉默地传播”,直到程序崩溃时才暴露,排查起来举步维艰。

为了解决这些痛点,C++ 引入了 “异常处理机制”。它将 “错误检测” 和 “错误处理” 彻底分离:程序的某个模块只需专注于 “发现错误时抛出异常”,而另一个模块则负责 “捕获异常并解决问题”,两者无需知道对方的具体实现细节。这种解耦的设计,让代码更具可读性、可维护性,也让错误处理变得更灵活、更全面。

接下来,我们将从基础概念出发,一步步拆解 C++ 异常的核心机制 —— 从异常的抛出与捕获,到栈展开的过程,再到异常安全、异常规范等实战要点,最后结合标准库异常体系和自定义异常案例,帮你彻底掌握 C++ 异常的使用。

一、C++ 异常的基础:什么是异常?

在 C++ 中,“异常” 是程序运行时发生的意外情况(比如除零、内存分配失败、数组越界等)的抽象表示 —— 它以 “对象” 的形式存在,能够携带丰富的错误信息(如错误描述、错误编号、发生位置等),并通过特定的语法(throwtrycatch)在函数调用链中传递,最终被对应的 “异常处理器” 捕获并处理。

异常处理的核心思想可以概括为三句话:

  1. 检测错误:在可能出错的地方,用throw抛出一个异常对象,告知 “这里出问题了”;
  2. 传递错误:异常会沿着函数调用链 “向上传播”(即 “栈展开”),直到找到能处理它的代码;
  3. 处理错误:用catch捕获异常,执行针对性的处理逻辑(比如打印错误信息、重试操作、释放资源等)。

1.1 为什么需要异常?对比 C 语言的错误码

为了更直观地理解异常的优势,我们先看一个 C 语言用错误码处理 “除零错误” 的例子:

#include <stdio.h>
#include <errno.h> // 用于存储错误码

// 用返回值表示结果,错误码存到errno中
double Divide(int a, int b) {
    if (b == 0) {
        errno = 1; // 用1表示除零错误(需要手动定义错误码含义)
        return -1.0; // 返回一个无效值
    }
    return (double)a / (double)b;
}

int main() {
    int a = 5, b = 0;
    double result = Divide(a, b);
    
    // 必须主动检查错误码,否则不知道出错
    if (errno != 0) {
        // 不知道具体错误原因,只能靠注释或查表
        printf("错误:除零错误\n");
        return 1;
    }
    printf("结果:%lf\n", result);
    return 0;
}

这个例子的问题很明显:

  • 错误信息有限errno只能存整数,想要知道 “具体是什么错”,必须手动维护错误码对照表(比如1=除零,2=内存不足);
  • 依赖主动检查:如果调用者忘记检查errno,错误会被忽略,程序可能继续执行错误逻辑;
  • 返回值歧义:返回-1.0可能是 “错误”,也可能是 “合法结果”(比如Divide(-1,1)),需要额外判断。

而用 C++ 异常处理同样的问题,代码会清晰很多:

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

double Divide(int a, int b) {
    if (b == 0) {
        // 抛出一个string对象,直接携带错误描述
        throw string("除零错误:除数b不能为0");
    }
    return (double)a / (double)b;
}

int main() {
    int a = 5, b = 0;
    try {
        // 尝试执行可能出错的代码
        double result = Divide(a, b);
        cout << "结果:" << result << endl;
    } catch (const string& errMsg) {
        // 捕获并处理异常,直接获取错误描述
        cout << "捕获到异常:" << errMsg << endl;
    }
    return 0;
}

运行结果:

对比可见,异常的优势:

  • 错误信息丰富:可以抛出任意类型的对象(如string、自定义类),携带错误描述、编号、位置等信息;
  • 强制处理(或传播):如果不捕获异常,它会自动向上传播,直到程序终止(不会 “沉默”);
  • 不占用返回值:返回值可以专注于 “正常结果”,无需用特殊值表示错误。

二、异常的核心操作:抛出与捕获

异常的使用依赖三个关键字:throw(抛出异常)、try(包裹可能抛出异常的代码)、catch(捕获并处理异常)。这三者的配合,是异常处理的基础。

2.1 抛出异常:throw关键字

throw的作用是 “触发异常”—— 当程序检测到错误时,用throw抛出一个 “异常对象”,告知系统 “这里发生了意外”。

语法与规则
  • 语法:throw 表达式;(表达式的结果就是异常对象,类型可以是内置类型、标准库类型或自定义类型);
  • 抛出后,throw后面的代码不会执行:程序会立即终止当前函数的执行,转而寻找能处理该异常的catch
  • 异常对象会被拷贝:如果抛出的是局部对象(比如函数内定义的string),系统会创建一个该对象的 “拷贝”,并将拷贝传递给catch(因为原局部对象会随着函数栈帧销毁而消失)—— 这个过程类似 “函数传值返回”。
常见的抛出场景
  1. 抛出内置类型:比如throw 1;(用整数表示错误编号),但信息有限,不推荐;
  2. 抛出标准库类型:比如throw string("错误描述");,简单直接,适合快速开发;
  3. 抛出自定义类型:比如throw SqlException("权限不足", 100, "select * from user");,能携带多维度信息,适合复杂项目。

示例:抛出自定义异常对象

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

// 自定义异常类,携带错误描述和编号
class MyException {
public:
    MyException(string msg, int id) : _msg(msg), _id(id) {}
    
    // 提供方法获取错误信息
    string getMsg() const { return _msg; }
    int getId() const { return _id; }

private:
    string _msg; // 错误描述
    int _id;     // 错误编号
};

// 模拟数据库操作,可能抛出异常
void queryDatabase(const string& sql) {
    // 模拟“权限不足”错误
    if (sql.find("select * from user") != string::npos) {
        throw MyException("权限不足:无法查询用户表", 1001);
    }
    cout << "SQL执行成功:" << sql << endl;
}

int main() {
    try {
        queryDatabase("select * from user"); // 会抛出异常
    } catch (const MyException& e) {
        // 捕获自定义异常,获取详细信息
        cout << "错误编号:" << e.getId() << endl;
        cout << "错误描述:" << e.getMsg() << endl;
    }
    return 0;
}

运行结果:

2.2 捕获异常:try-catch

try-catch块的作用是 “捕获并处理异常”——try包裹 “可能抛出异常的代码”,catch则定义 “如何处理特定类型的异常”。

语法结构

cpp

try {
    // 可能抛出异常的代码(如函数调用、危险操作)
    riskyOperation();
} catch (类型1& e) {
    // 处理“类型1”的异常
} catch (类型2& e) {
    // 处理“类型2”的异常
} catch (...) {
    // 处理所有其他类型的异常(兜底,避免程序终止)
}
关键规则
  1. try不能单独存在:必须跟至少一个catch(包括catch(...));
  2. 匹配规则:类型优先catch只会捕获 “类型完全匹配”(或符合特殊转换规则,后面会讲)的异常;
  3. 顺序重要:如果有多个catch子类异常的catch必须放在父类异常的catch前面(否则父类catch会先匹配,子类catch永远不会执行);
  4. catch(...)是兜底:它能捕获任意类型的异常,但无法获取异常对象的具体信息,通常用于 “防止程序崩溃”,并记录 “未知异常” 日志。
示例:多catch的匹配顺序
#include <iostream>
#include <string>
using namespace std;

// 基类异常
class BaseException {
public:
    virtual string what() const { return "BaseException"; }
};

// 子类异常:除零错误
class DivideZeroException : public BaseException {
public:
    string what() const override { return "DivideZeroException:除零错误"; }
};

// 子类异常:空指针错误
class NullPtrException : public BaseException {
public:
    string what() const override { return "NullPtrException:空指针错误"; }
};

// 可能抛出不同异常的函数
void doSomething(int type) {
    if (type == 1) {
        throw DivideZeroException();
    } else if (type == 2) {
        throw NullPtrException();
    } else {
        throw BaseException();
    }
}

int main() {
    int type;
    cin >> type;

    try {
        doSomething(type);
    } catch (const DivideZeroException& e) { // 子类1:先捕获
        cout << "处理除零错误:" << e.what() << endl;
    } catch (const NullPtrException& e) {   // 子类2:再捕获
        cout << "处理空指针错误:" << e.what() << endl;
    } catch (const BaseException& e) {       // 父类:后捕获
        cout << "处理基类异常:" << e.what() << endl;
    } catch (...) {                          // 兜底:最后捕获
        cout << "处理未知异常" << endl;
    }

    return 0;
}

测试结果:

  • 输入1处理除零错误:DivideZeroException:除零错误
  • 输入2处理空指针错误:NullPtrException:空指针错误
  • 输入3处理基类异常:BaseException

如果把BaseExceptioncatch放在最前面,输入12时,都会被BaseExceptioncatch捕获(因为子类对象可以隐式转换为父类对象),导致子类catch失效 —— 这就是 “顺序重要” 的原因。

三、异常的传播:栈展开(Stack Unwinding)

throw抛出异常后,如果当前函数的try-catch块无法匹配该异常,程序会怎么做?答案是 “栈展开”—— 沿着函数调用链 “向上回溯”,逐个销毁函数栈帧,直到找到能匹配的catch,或者到达main函数后终止程序。

3.1 什么是 “函数栈帧”?

在理解栈展开前,我们需要先明确 “函数栈帧” 的概念:程序运行时,函数的调用会在 “调用栈”(栈内存)中创建一个 “栈帧”,用于存储函数的参数、局部变量、返回地址等信息。当函数执行完毕,栈帧会被销毁,内存释放。

比如,main调用func3func3调用func2func2调用func1,此时的调用栈和栈帧如下:

调用栈(从上到下,栈增长方向向下):
[func1的栈帧:局部变量、参数等]
[func2的栈帧:局部变量、参数等]
[func3的栈帧:局部变量、参数等]
[main的栈帧:局部变量、参数等]

3.2 栈展开的完整过程

栈展开的核心是 “逐层回溯、销毁栈帧、寻找匹配的catch”,具体步骤如下:

  1. 抛出异常:在函数Athrow一个异常对象;
  2. 检查当前函数:查看throw是否在Atry块内:
    • 如果在,遍历Acatch,寻找类型匹配的处理器;
    • 如果找到,执行catch代码,之后继续执行catch块后面的代码;
    • 如果没找到,进入下一步;
  3. 销毁当前栈帧:销毁A的栈帧(局部变量、参数等被释放),退回到调用A的函数B
  4. 重复检查:在B中重复步骤 2-3,直到找到匹配的catch
  5. 终止程序:如果回溯到main函数,仍未找到匹配的catch,系统会调用terminate()函数,强制终止程序。

3.3 栈展开的实例分析

我们用一个完整的代码例子,一步一步拆解栈展开的过程:

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

// 函数调用链:main -> func3 -> func2 -> func1
void func1() {
    cout << "进入func1,准备抛出异常" << endl;
    // 抛出string类型的异常
    throw string("func1中发生的错误");
    cout << "func1中throw后面的代码(不会执行)" << endl;
}

void func2() {
    cout << "进入func2,调用func1" << endl;
    func1(); // 调用func1,会抛出异常
    cout << "func2中func1后面的代码(不会执行)" << endl;
}

void func3() {
    cout << "进入func3,调用func2" << endl;
    // func3的try-catch:只捕获int类型,无法匹配string
    try {
        func2();
    } catch (int errId) {
        cout << "func3捕获到int异常:" << errId << endl;
    }
    cout << "func3中try-catch后面的代码(异常未被捕获,不会执行)" << endl;
}

int main() {
    cout << "进入main,调用func3" << endl;
    try {
        func3(); // 调用func3,最终会传播异常到这里
    } catch (const string& errMsg) {
        // main的catch:匹配string类型,捕获异常
        cout << "main捕获到string异常:" << errMsg << endl;
    } catch (...) {
        cout << "main捕获到未知异常" << endl;
    }
    cout << "main中try-catch后面的代码(会执行)" << endl;
    return 0;
}
步骤 1:函数调用与异常抛出
  • main调用func3func3调用func2func2调用func1
  • func1中执行throw string("func1中发生的错误")throw后面的代码不会执行。
步骤 2:func1 中无 try-catch,销毁栈帧
  • func1没有try-catch块,直接销毁func1的栈帧,退回到func2
  • func2func1()后面的代码不会执行(因为异常未处理)。
步骤 3:func2 中无 try-catch,销毁栈帧
  • func2也没有try-catch块,销毁func2的栈帧,退回到func3
步骤 4:func3 的 try-catch 不匹配,销毁栈帧
  • func3try-catch,但catch的是int类型,无法匹配string异常;
  • 销毁func3的栈帧,退回到main
步骤 5:main 的 try-catch 匹配,处理异常
  • maintry块包裹func3()catch (const string& errMsg)匹配异常;
  • 执行catch中的代码,打印 “main 捕获到 string 异常:func1 中发生的错误”;
  • 执行catch块后面的代码,打印 “main 中 try-catch 后面的代码(会执行)”。
最终运行结果

如果我们删除main中的catch (const string& errMsg),会发生什么?

  • 异常会传播到maincatch (...),打印 “main 捕获到未知异常”;
  • 如果连catch (...)也删除,main中没有匹配的catch,程序会调用terminate(),直接终止,不会执行main后面的代码。

四、异常的匹配规则:不止 “完全匹配”

前面我们提到,catch的匹配规则是 “类型优先”,但这并不意味着只有 “完全相同的类型” 才能匹配。C++ 允许三种特殊的类型转换,让catch能匹配 “兼容类型” 的异常。

4.1 允许的三种类型转换

1. 非常量(non-const)到常量(const)的转换(权限缩小)

如果抛出的是 “非常量对象”,catch可以用 “常量引用” 捕获 —— 这是最常见的转换,因为捕获引用可以避免对象拷贝,而const可以防止误修改异常对象。

示例:

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

void throwNonConst() {
    string err = "非常量string异常";
    throw err; // 抛出非常量string对象
}

int main() {
    try {
        throwNonConst();
    } catch (const string& e) { // 用const引用捕获,允许转换
        cout << "捕获成功:" << e << endl;
    }
    return 0;
}

运行结果:捕获成功:非常量string异常

2. 数组到指针、函数到函数指针的转换

如果抛出的是 “数组”,catch可以用 “指向数组元素类型的指针” 捕获;如果抛出的是 “函数”,catch可以用 “函数指针” 捕获。

示例:数组到指针的转换

#include <iostream>
using namespace std;

void throwArray() {
    const char err[] = "数组类型的异常";
    throw err; // 抛出数组,会被转换为const char*
}

int main() {
    try {
        throwArray();
    } catch (const char* e) { // 用指针捕获数组异常
        cout << "捕获成功:" << e << endl;
    }
    return 0;
}

运行结果:捕获成功:数组类型的异常

3. 派生类到基类的转换(最实用的转换)

如果抛出的是 “派生类异常对象”,catch可以用 “基类引用或指针” 捕获 —— 这是 C++ 异常处理中最核心的特性之一,也是 “自定义异常体系” 的设计基础。

通过这种转换,我们可以用一个catch (BaseException& e)捕获所有 “继承自 BaseException 的子类异常”,大大简化代码。

示例:派生类到基类的转换

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

// 基类异常
class Exception {
public:
    Exception(string msg) : _msg(msg) {}
    virtual string what() const { return _msg; } // 虚函数,支持多态

private:
    string _msg;
};

// 子类异常:SQL错误
class SqlException : public Exception {
public:
    SqlException(string msg, string sql) 
        : Exception(msg), _sql(sql) {}
    
    // 重写what(),返回更详细的信息
    string what() const override {
        return "SqlException:" + Exception::what() + ",SQL:" + _sql;
    }

private:
    string _sql;
};

// 子类异常:HTTP错误
class HttpException : public Exception {
public:
    HttpException(string msg, string method) 
        : Exception(msg), _method(method) {}
    
    string what() const override {
        return "HttpException:" + Exception::what() + ",方法:" + _method;
    }

private:
    string _method;
};

// 模拟SQL操作,抛出SqlException
void sqlQuery(const string& sql) {
    if (sql.empty()) {
        throw SqlException("SQL语句为空", sql);
    }
}

// 模拟HTTP请求,抛出HttpException
void httpRequest(const string& method) {
    if (method != "GET" && method != "POST") {
        throw HttpException("不支持的请求方法", method);
    }
}

int main() {
    try {
        // 测试1:抛出SqlException
        // sqlQuery(""); 
        // 测试2:抛出HttpException
        httpRequest("PUT"); 
    } catch (const Exception& e) { // 用基类引用捕获所有子类异常
        // 多态调用:根据实际异常类型,调用对应的what()
        cout << "捕获到异常:" << e.what() << endl;
    }

    return 0;
}

测试结果(开启httpRequest("PUT")):

测试结果(开启sqlQuery("")):

这个例子完美体现了 “派生类到基类转换” 的优势:无论抛出的是SqlException还是HttpException,只需一个catch (const Exception& e)就能处理,且通过虚函数what()实现了 “多态打印”,获取不同异常的详细信息。

4.2 匹配规则的优先级

当多个catch都能匹配异常时,优先级如下:

  1. 完全匹配:比任何转换都优先;
  2. 派生类到基类的转换:如果没有完全匹配,再看是否能转换为基类;
  3. catch(...):最后匹配,兜底用。

示例:优先级验证

#include <iostream>
using namespace std;

class Base {};
class Derived : public Base {};

int main() {
    try {
        throw Derived(); // 抛出派生类对象
    } catch (const Derived& e) { // 完全匹配:优先级最高
        cout << "捕获到Derived异常" << endl;
    } catch (const Base& e) { // 派生类到基类:优先级次之
        cout << "捕获到Base异常" << endl;
    } catch (...) { // 兜底:优先级最低
        cout << "捕获到未知异常" << endl;
    }
    return 0;
}

运行结果:捕获到Derived异常

如果删除catch (const Derived& e),结果会变成捕获到Base异常—— 这就是优先级的体现。

五、异常的二次处理:异常重新抛出

在实际开发中,我们可能需要 “部分处理异常”:比如捕获异常后,先记录日志,再将异常传递给上层函数处理;或者只处理特定类型的异常,其他异常继续向上传播。这时,就需要 “异常重新抛出”。

5.1 重新抛出的语法:throw;

重新抛出异常的语法非常简单:在catch块中直接写throw;,它会 “原封不动” 地将当前捕获的异常对象再次抛出。

注意:不要用throw e;(e 是捕获的异常对象),因为这会创建一个新的异常对象(拷贝 e),可能导致 “切片问题”(如果 e 是基类引用,实际指向子类对象,throw e;会抛出基类对象,丢失子类特有的信息)。

示例:throw; vs throw e;的区别

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

class BaseException {
public:
    virtual string what() const { return "BaseException"; }
};

class DerivedException : public BaseException {
public:
    string what() const override { return "DerivedException"; }
};

void rethrowTest() {
    try {
        throw DerivedException(); // 抛出子类对象
    } catch (const BaseException& e) {
        cout << "rethrowTest中捕获到:" << e.what() << endl;
        
        // 错误方式:throw e; 会抛出BaseException对象(切片)
        // throw e; 
        
        // 正确方式:throw; 会抛出原DerivedException对象
        throw; 
    }
}

int main() {
    try {
        rethrowTest();
    } catch (const DerivedException& e) {
        cout << "main中捕获到DerivedException:" << e.what() << endl;
    } catch (const BaseException& e) {
        cout << "main中捕获到BaseException:" << e.what() << endl;
    }
    return 0;
}
情况 1:用throw e;(错误)

运行结果:

原因:throw e;拷贝了e(基类引用,指向子类对象),但抛出的是BaseException对象,子类信息丢失(切片)。

情况 2:用throw;(正确)

运行结果:

原因:throw;直接抛出原异常对象(DerivedException),没有拷贝,信息完整。

5.2 重新抛出的典型场景:重试机制

重新抛出最常见的场景之一是 “重试”—— 比如网络请求失败时,重试几次后再抛异常;如果重试成功,则不抛异常。

示例:网络请求重试

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

// 自定义HTTP异常
class HttpException {
public:
    HttpException(string msg, int errId) : _msg(msg), _errId(errId) {}
    string getMsg() const { return _msg; }
    int getErrId() const { return _errId; }

private:
    string _msg;
    int _errId;
};

// 模拟发送网络请求,50%概率失败(errId=102:网络不稳定)
void sendRequest(const string& data) {
    srand(time(0)); // 初始化随机数种子
    if (rand() % 2 == 0) {
        throw HttpException("网络不稳定,请求失败", 102);
    }
    cout << "请求发送成功,数据:" << data << endl;
}

// 封装重试逻辑:最多重试3次
void sendWithRetry(const string& data) {
    const int maxRetry = 3; // 最大重试次数
    for (int i = 0; i < maxRetry; ++i) {
        try {
            sendRequest(data);
            return; // 发送成功,直接返回
        } catch (const HttpException& e) {
            // 只处理errId=102(网络不稳定),其他错误直接抛出
            if (e.getErrId() != 102) {
                throw; // 非网络错误,重新抛出
            }
            // 网络错误,重试
            cout << "第" << (i+1) << "次请求失败:" << e.getMsg() << ",准备重试..." << endl;
        }
    }
    // 重试3次都失败,抛出最终异常
    throw HttpException("重试3次后仍失败:网络持续不稳定", 102);
}

int main() {
    try {
        sendWithRetry("Hello, Server!");
    } catch (const HttpException& e) {
        cout << "最终失败:" << e.getMsg() << "(错误编号:" << e.getErrId() << ")" << endl;
    }
    return 0;
}

可能的运行结果:

这个例子中,sendWithRetry捕获HttpException后,只对 “网络不稳定”(errId=102)进行重试,其他错误(比如 “权限不足”)直接重新抛出,交给main处理 —— 这就是 “部分处理 + 重新抛出” 的典型用法。

六、异常的隐藏陷阱:异常安全问题

异常虽然强大,但也会带来 “异常安全” 问题 —— 如果异常抛出时,程序已经申请了资源(比如内存、文件句柄、锁),但还没来得及释放,这些资源会因为栈展开时局部变量被销毁而永久泄漏,导致程序运行不稳定。

6.1 常见的异常安全问题:资源泄漏

最典型的资源泄漏场景是 “手动申请内存后,异常抛出导致未释放”。

示例:内存泄漏问题

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

double Divide(int a, int b) {
    if (b == 0) {
        throw string("除零错误");
    }
    return (double)a / (double)b;
}

void func() {
    // 申请内存(资源)
    int* arr = new int[10]; 
    cout << "内存申请成功:" << arr << endl;

    // 执行可能抛出异常的操作
    int a = 5, b = 0;
    double result = Divide(a, b); // 抛出异常
    cout << "计算结果:" << result << endl;

    // 释放内存(异常抛出后,这行代码不会执行!)
    delete[] arr;
    cout << "内存释放成功" << endl;
}

int main() {
    try {
        func();
    } catch (const string& e) {
        cout << "捕获到异常:" << e << endl;
    }

    // 程序结束后,arr指向的内存仍未释放(内存泄漏)
    return 0;
}

运行结果:

问题分析:funcnew int[10]申请了内存,但Divide(a,b)抛出异常后,delete[] arr不会执行,导致arr指向的内存永远无法释放 —— 这就是内存泄漏。

6.2 临时解决方案:在catch中释放资源

要解决这个问题,我们可以在func中添加try-catch块,捕获异常后先释放资源,再重新抛出异常。

示例:修复内存泄漏

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

double Divide(int a, int b) {
    if (b == 0) {
        throw string("除零错误");
    }
    return (double)a / (double)b;
}

void func() {
    int* arr = new int[10];
    cout << "内存申请成功:" << arr << endl;

    try {
        // 把可能抛出异常的代码放在try块中
        int a = 5, b = 0;
        double result = Divide(a, b);
        cout << "计算结果:" << result << endl;
    } catch (...) {
        // 捕获所有异常,先释放资源
        delete[] arr;
        cout << "异常发生,释放内存:" << arr << endl;
        throw; // 重新抛出异常,交给上层处理
    }

    // 正常执行时,释放资源
    delete[] arr;
    cout << "正常执行,释放内存:" << arr << endl;
}

int main() {
    try {
        func();
    } catch (const string& e) {
        cout << "捕获到异常:" << e << endl;
    }
    return 0;
}

运行结果:

这样一来,无论是否发生异常,内存都会被释放 —— 异常安全问题得到解决。但这种方式的缺点很明显:如果有多个资源(比如内存、文件、锁),需要写大量的try-catch,代码臃肿且容易出错。

6.3 终极解决方案:RAII(资源获取即初始化)

RAII(Resource Acquisition Is Initialization)是 C++ 中解决异常安全问题的 “银弹”—— 它的核心思想是 “将资源的生命周期与对象的生命周期绑定”:

  • 资源获取(比如newopen)在对象的构造函数中完成;
  • 资源释放(比如deleteclose)在对象的析构函数中完成;
  • 当对象离开作用域(比如栈展开时局部对象被销毁),析构函数会自动调用,资源被释放。

C++ 标准库中的 “智能指针”(unique_ptrshared_ptr)就是 RAII 的典型实现。用智能指针管理内存,即使发生异常,也能自动释放资源。

示例:用智能指针解决内存泄漏

#include <iostream>
#include <string>
#include <memory> // 包含智能指针头文件
using namespace std;

double Divide(int a, int b) {
    if (b == 0) {
        throw string("除零错误");
    }
    return (double)a / (double)b;
}

void func() {
    // 用unique_ptr管理内存(RAII)
    unique_ptr<int[]> arr(new int[10]); 
    cout << "内存申请成功(智能指针管理)" << endl;

    // 执行可能抛出异常的操作,无需手动释放内存
    int a = 5, b = 0;
    double result = Divide(a, b);
    cout << "计算结果:" << result << endl;

    // 无需手动delete,arr离开作用域时,析构函数自动释放内存
}

int main() {
    try {
        func();
    } catch (const string& e) {
        cout << "捕获到异常:" << e << endl;
    }
    return 0;
}
内存申请成功(智能指针管理)
捕获到异常:除零错误

问题分析:unique_ptr<int[]> arr创建后,new int[10]的内存由arr管理。当func中抛出异常,arr作为局部对象会被销毁,其析构函数会自动调用delete[]释放内存 —— 无需手动写try-catch,代码简洁且安全。

6.4 另一个陷阱:析构函数不能抛出异常

除了资源泄漏,“析构函数抛出异常” 也是一个严重的异常安全问题。因为析构函数的作用是 “释放资源”,如果析构函数抛出异常,可能导致:

  • 后续的资源释放操作无法执行(比如析构函数要释放 10 个资源,第 5 个时抛出异常,后面 5 个资源泄漏);
  • 程序调用terminate()终止(如果析构函数的异常在栈展开过程中抛出)。

《Effective C++》第 8 条明确指出:别让异常逃离析构函数

示例:析构函数抛出异常的风险

#include <iostream>
using namespace std;

class BadResource {
public:
    BadResource(int id) : _id(id) {
        cout << "构造函数:创建资源" << _id << endl;
    }

    ~BadResource() {
        cout << "析构函数:释放资源" << _id << endl;
        // 模拟析构函数抛出异常
        throw "析构函数抛出异常";
    }

private:
    int _id;
};

int main() {
    try {
        BadResource res1(1);
        BadResource res2(2);
        // 抛出异常,触发栈展开,销毁res2和res1
        throw "main中抛出异常";
    } catch (const char* e) {
        cout << "捕获到异常:" << e << endl;
    }
    return 0;
}

运行结果:

问题分析:

  1. main中抛出异常,触发栈展开,先销毁res2
  2. res2的析构函数抛出异常,而此时正处于 “处理 main 异常” 的栈展开过程中;
  3. C++ 不允许 “异常嵌套”,直接调用terminate()终止程序,res1的析构函数没有执行,资源 1 泄漏。
解决方案:在析构函数中捕获异常

如果析构函数中确实可能发生异常(比如关闭文件时失败),必须在析构函数内部捕获并处理,不让异常逃离。

示例:安全的析构函数

#include <iostream>
using namespace std;

class SafeResource {
public:
    SafeResource(int id) : _id(id) {
        cout << "构造函数:创建资源" << _id << endl;
    }

    ~SafeResource() {
        try {
            cout << "析构函数:释放资源" << _id << endl;
            // 模拟可能抛出异常的操作
            if (_id == 2) {
                throw "关闭文件失败";
            }
        } catch (const char* e) {
            // 内部捕获异常,记录日志,不让异常逃离
            cout << "析构函数内部处理异常:" << e << endl;
        }
    }

private:
    int _id;
};

int main() {
    try {
        SafeResource res1(1);
        SafeResource res2(2);
        throw "main中抛出异常";
    } catch (const char* e) {
        cout << "main捕获到异常:" << e << endl;
    }
    return 0;
}

运行结果:

这样一来,析构函数的异常被内部处理,不会影响栈展开,所有资源都能正常释放,程序也不会终止。

七、异常的 “说明书”:异常规范

在团队开发中,我们需要告诉其他开发者:“我的函数会不会抛出异常?会抛出哪些类型的异常?”—— 这就是 “异常规范” 的作用。它相当于函数的 “说明书”,帮助调用者提前做好异常处理准备。

C++ 历史上有两种异常规范:C++98 的throw()语法和 C++11 的noexcept语法。目前,noexcept已基本取代throw(),成为主流。

7.1 C++98 的异常规范:throw()

C++98 的异常规范用throw(类型列表)来声明函数可能抛出的异常类型:

  • throw(Type1, Type2):函数可能抛出Type1Type2类型的异常;
  • throw():函数不会抛出任何异常(空列表);
  • 不写:函数可能抛出任意类型的异常。

示例:C++98 异常规范

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

// 声明:可能抛出string或int类型的异常
void func1() throw(string, int) {
    if (rand() % 2 == 0) {
        throw string("string类型异常");
    } else {
        throw 100;
    }
}

// 声明:不会抛出任何异常
void func2() throw() {
    cout << "func2:不会抛出异常" << endl;
}

int main() {
    srand(time(0));
    try {
        func1();
    } catch (const string& e) {
        cout << "捕获到string异常:" << e << endl;
    } catch (int e) {
        cout << "捕获到int异常:" << e << endl;
    }

    func2();
    return 0;
}
C++98 异常规范的缺陷
  • 语法繁琐:如果函数可能抛出多种类型,throw()列表会很长;
  • 编译器检查宽松:即使声明throw(string),函数中抛出int异常,编译器也不会报错(只会在运行时调用unexpected()终止程序);
  • 兼容性差:不同编译器对throw()的处理不一致,导致跨平台问题。

正因如此,C++11 引入了更简洁、更实用的noexcept

7.2 C++11 的异常规范:noexcept

C++11 用noexcept关键字简化了异常规范,规则更清晰:

  • noexcept:声明函数不会抛出任何异常;
  • noexcept(表达式):根据 “表达式是否可能抛出异常” 来决定函数是否 noexcept(表达式为true则不抛,false则可能抛);
  • 不写noexcept:函数可能抛出任何异常(默认行为)。
1. noexcept的基本用法
#include <iostream>
#include <string>
using namespace std;

// 声明:不会抛出异常
void func1() noexcept {
    cout << "func1:不会抛出异常" << endl;
}

// 声明:可能抛出异常(默认,可省略)
void func2() {
    if (rand() % 2 == 0) {
        throw string("func2抛出异常");
    }
    cout << "func2:未抛出异常" << endl;
}

int main() {
    srand(time(0));
    func1(); // 安全调用,无需try-catch

    try {
        func2();
    } catch (const string& e) {
        cout << "捕获到func2的异常:" << e << endl;
    }
    return 0;
}
2. noexcept的编译器行为
  • 不强制检查:即使函数声明了noexcept,如果函数内部抛出异常,编译器也不会报错(但会在运行时调用terminate()终止程序);
  • 优化提示:编译器会根据noexcept进行优化 —— 比如noexcept函数不需要生成 “异常处理相关的代码”,执行效率更高。

示例:noexcept函数抛出异常的后果

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

// 声明noexcept,但内部抛出异常
void func() noexcept {
    throw string("noexcept函数抛出异常");
}

int main() {
    try {
        func();
    } catch (const string& e) {
        // 永远不会执行到这里,因为func是noexcept
        cout << "捕获到异常:" << e << endl;
    }
    return 0;
}

运行结果:

因此,必须确保noexcept函数内部确实不会抛出异常(包括直接抛出和间接调用可能抛出异常的函数)。

3. noexcept作为运算符

noexcept还可以作为 “运算符” 使用,用于检测一个 “表达式是否可能抛出异常”,返回bool类型(true表示不会抛,false表示可能抛)。

示例:noexcept运算符

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

void func1() noexcept { cout << "func1" << endl; }
void func2() { throw string("func2"); }

int main() {
    // 检测表达式是否可能抛出异常
    cout << "noexcept(func1()):" << noexcept(func1()) << endl; // true(func1是noexcept)
    cout << "noexcept(func2()):" << noexcept(func2()) << endl; // false(func2可能抛)
    cout << "noexcept(1 + 2):" << noexcept(1 + 2) << endl;     // true(内置运算不会抛)
    cout << "noexcept(vector<int>().at(0)):" << noexcept(vector<int>().at(0)) << endl; // false(at()可能抛out_of_range)

    return 0;
}

运行结果:

noexcept(func1()):1
noexcept(func2()):0
noexcept(1 + 2):1
noexcept(vector<int>().at(0)):0

noexcept运算符常用于模板编程中,根据模板参数的特性决定函数是否 noexcept。

八、C++ 标准库的异常体系

为了方便开发者使用,C++ 标准库提供了一套预定义的异常体系,所有异常类都继承自基类std::exception。这个体系覆盖了大多数常见的错误场景,比如内存分配失败、类型转换错误、逻辑错误等。

8.1 标准库异常的继承结构

std::exception是所有标准库异常的基类,定义在<exception>头文件中,它只有一个虚函数what(),返回const char*类型的错误描述,子类可以重写该函数。

标准库异常的继承结构如下(关键类):

std::exception(基类)
├─ std::bad_alloc(内存分配失败,如new失败)
├─ std::bad_cast(动态类型转换失败,如dynamic_cast<void*>失败)
├─ std::bad_typeid(typeid操作空指针失败)
├─ std::bad_exception(异常处理过程中发生的意外异常)
├─ std::logic_error(逻辑错误:编译时可检测,如参数无效)
│  ├─ std::invalid_argument(无效参数,如传递空指针给需要非空的函数)
│  ├─ std::domain_error(参数不在合法范围内,如数学函数的定义域错误)
│  ├─ std::length_error(长度超出范围,如string::reserve(过大值))
│  └─ std::out_of_range(索引超出范围,如vector::at(超出大小的索引))
└─ std::runtime_error(运行时错误:编译时不可检测,如文件不存在)
   ├─ std::range_error(运行时范围错误,如计算结果超出表示范围)
   ├─ std::overflow_error(算术上溢,如int最大值+1)
   └─ std::underflow_error(算术下溢,如int最小值-1)

8.2 常用标准库异常的使用示例

我们通过几个例子,学习如何使用标准库异常。

1. std::bad_alloc:内存分配失败

new无法分配足够的内存时,会抛出std::bad_alloc异常(注意:new(nothrow)不会抛出异常,而是返回nullptr)。

示例:

#include <iostream>
#include <new> // 包含bad_alloc
using namespace std;

int main() {
    try {
        // 尝试分配一个极大的内存(超过系统可用内存)
        int* p = new int[1000000000000]; 
        delete[] p;
    } catch (const bad_alloc& e) {
        // 捕获内存分配失败异常
        cout << "捕获到bad_alloc:" << e.what() << endl;
    }
    return 0;
}

运行结果:

2. std::out_of_range:索引超出范围

vector::at()string::at()等成员函数会检查索引是否超出范围,超出时抛出std::out_of_range异常(而operator[]不检查,会导致未定义行为)。

示例:

#include <iostream>
#include <vector>
#include <stdexcept> // 包含out_of_range
using namespace std;

int main() {
    vector<int> vec = {1, 2, 3};

    try {
        // vec的大小是3,索引0-2,at(3)超出范围
        cout << vec.at(3) << endl;
    } catch (const out_of_range& e) {
        cout << "捕获到out_of_range:" << e.what() << endl;
    }

    // 对比:operator[]不检查,行为未定义(可能崩溃或输出垃圾值)
    // cout << vec[3] << endl;

    return 0;
}

运行结果:

捕获到out_of_range:vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)
3. std::invalid_argument:无效参数

当传递给函数的参数不符合要求时,会抛出std::invalid_argument异常(常见于字符串转换、算法函数等)。

示例:

#include <iostream>
#include <string>
#include <sstream>
#include <stdexcept> // 包含invalid_argument
using namespace std;

// 把字符串转换为整数,空字符串视为无效参数
int stringToInt(const string& s) {
    if (s.empty()) {
        throw invalid_argument("无效参数:字符串为空");
    }
    stringstream ss(s);
    int num;
    ss >> num;
    if (ss.fail()) {
        throw invalid_argument("无效参数:无法转换为整数");
    }
    return num;
}

int main() {
    try {
        cout << stringToInt("123") << endl; // 正常转换
        cout << stringToInt("") << endl;    // 抛出无效参数异常
    } catch (const invalid_argument& e) {
        cout << "捕获到invalid_argument:" << e.what() << endl;
    }
    return 0;
}

运行结果:

123
捕获到invalid_argument:无效参数:字符串为空

8.3 自定义异常继承标准库异常

在实际开发中,我们可以继承标准库的异常类(比如std::exceptionstd::runtime_error),实现自定义异常 —— 这样做的好处是,自定义异常可以和标准库异常一起被catch (const std::exception& e)捕获,统一处理。

示例:自定义 HTTP 异常(继承std::runtime_error

#include <iostream>
#include <string>
#include <stdexcept> // 包含runtime_error
using namespace std;

// 自定义HTTP异常,继承自std::runtime_error
class HttpException : public runtime_error {
public:
    // 构造函数:调用父类runtime_error的构造函数(传递错误描述)
    HttpException(int statusCode, const string& msg) 
        : runtime_error(msg), _statusCode(statusCode) {}
    
    // 获取HTTP状态码(自定义属性)
    int getStatusCode() const { return _statusCode; }

private:
    int _statusCode; // HTTP状态码(如404、500)
};

// 模拟HTTP请求,根据路径抛出不同异常
void httpGet(const string& path) {
    if (path == "/404") {
        throw HttpException(404, "页面未找到:" + path);
    } else if (path == "/500") {
        throw HttpException(500, "服务器内部错误:" + path);
    }
    cout << "HTTP GET成功:" << path << endl;
}

int main() {
    try {
        httpGet("/home"); // 正常
        httpGet("/404");  // 抛出404异常
    } catch (const HttpException& e) {
        // 捕获自定义异常,获取状态码和描述
        cout << "HTTP错误 " << e.getStatusCode() << ":" << e.what() << endl;
    } catch (const exception& e) {
        // 捕获其他标准库异常
        cout << "标准库异常:" << e.what() << endl;
    }
    return 0;
}

运行结果:

HTTP GET成功:/home
HTTP错误 404:页面未找到:/404

这种方式的优势在于:自定义异常融入了标准库异常体系,既可以单独捕获,也可以和其他标准库异常一起用catch (const exception& e)统一处理,灵活性更高。

九、实战:构建一个完整的异常处理体系

前面我们学习了异常的基础概念、核心机制和实战要点,现在我们将这些知识结合起来,构建一个 “服务端程序的异常处理体系”—— 包含 SQL、缓存、HTTP 三个模块,每个模块抛出自定义异常,通过基类统一捕获,同时处理异常安全问题。

9.1 需求分析

  • 模块划分:HTTP 模块(处理请求)、缓存模块(处理缓存读写)、SQL 模块(处理数据库操作),模块间依赖关系为:HTTP 模块调用缓存模块,缓存模块调用 SQL 模块;
  • 异常体系:设计基类Exception,子类HttpExceptionCacheExceptionSqlException分别对应三个模块的异常,子类重写what()方法以提供模块专属的错误信息;
  • 异常安全:使用智能指针(shared_ptr)管理模块对象生命周期,避免手动释放资源导致的泄漏;析构函数声明为noexcept,确保不抛出异常;
  • 运行逻辑:程序以 1 秒为周期循环模拟 HTTP 请求,随机触发各模块异常,在main函数中统一捕获并打印异常信息,确保程序不终止,持续提供服务。

9.2 完整代码实现(基于异常体系规范)

#include <iostream>
#include <string>
#include <memory>   // 智能指针,解决资源泄漏(RAII)
#include <ctime>    // 随机数种子初始化
#include <thread>   // 线程睡眠,模拟服务周期
#include <chrono>   // 时间单位(秒)
#include <cstring>  // 时间格式化
using namespace std;

// -------------------------- 1. 自定义异常体系(基类+子类) --------------------------
// 异常基类:所有模块异常的父类
class Exception {
public:
    // 构造函数:初始化错误描述、错误编号、错误发生时间
    Exception(const string& errmsg, int id)
        : _errmsg(errmsg)
        , _id(id)
        , _timestamp(time(nullptr))  // 获取当前时间戳
    {}

    // 析构函数:声明为noexcept,确保不抛出异常(避免栈展开时程序终止)
    virtual ~Exception() noexcept {}

    // 虚函数:返回错误信息(多态调用,子类可重写)
    virtual string what() const {
        // 格式化时间戳(转换为"年-月-日 时:分:秒")
        char time_buf[64] = {0};
        strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", localtime(&_timestamp));
        // 拼接基础错误信息
        return "[" + string(time_buf) + "] 错误ID:" + to_string(_id) + ",描述:" + _errmsg;
    }

    // 获取错误ID(用于异常分类处理,如重试逻辑)
    int getid() const { return _id; }

protected:
    string _errmsg;       // 错误描述
    int _id;              // 错误编号(区分不同错误类型)
    time_t _timestamp;    // 错误发生时间戳
};

// SQL模块异常:继承自Exception,增加SQL语句信息
class SqlException : public Exception {
public:
    // 构造函数:调用基类构造,初始化SQL语句
    SqlException(const string& errmsg, int id, const string& sql)
        : Exception(errmsg, id)
        , _sql(sql)
    {}

    // 重写what():返回包含SQL语句的错误信息
    virtual string what() const override {
        string base_info = Exception::what();
        return "SqlException:" + base_info + ",SQL语句:" + _sql;
    }

private:
    const string _sql;  // 触发错误的SQL语句(便于定位问题)
};

// 缓存模块异常:继承自Exception,增加缓存键信息
class CacheException : public Exception {
public:
    // 构造函数:调用基类构造,初始化缓存键
    CacheException(const string& errmsg, int id, const string& key)
        : Exception(errmsg, id)
        , _key(key)
    {}

    // 重写what():返回包含缓存键的错误信息
    virtual string what() const override {
        string base_info = Exception::what();
        return "CacheException:" + base_info + ",缓存键:" + _key;
    }

private:
    const string _key;  // 触发错误的缓存键(便于定位问题)
};

// HTTP模块异常:继承自Exception,增加请求方法和路径信息
class HttpException : public Exception {
public:
    // 构造函数:调用基类构造,初始化请求方法和路径
    HttpException(const string& errmsg, int id, const string& method, const string& path)
        : Exception(errmsg, id)
        , _method(method)
        , _path(path)
    {}

    // 重写what():返回包含HTTP请求信息的错误信息
    virtual string what() const override {
        string base_info = Exception::what();
        return "HttpException:" + base_info + ",请求方法:" + _method + ",路径:" + _path;
    }

private:
    const string _method;  // HTTP请求方法(GET/POST等)
    const string _path;    // HTTP请求路径(如/query、/login等)
};

// -------------------------- 2. 业务模块实现(依赖注入+智能指针) --------------------------
// SQL模块:模拟数据库查询操作,随机触发异常
class SqlMgr {
public:
    // 模拟SQL查询:1/7概率触发"权限不足",1/7概率触发"语法错误"
    void query(const string& sql) {
        int rand_val = rand() % 7;  // 生成0-6的随机数
        if (rand_val == 0) {
            // 抛出"权限不足"异常(错误ID:1001)
            throw SqlException("权限不足:无法访问目标表", 1001, sql);
        } else if (rand_val == 1) {
            // 抛出"SQL语法错误"异常(错误ID:1002)
            throw SqlException("语法错误:关键字拼写错误", 1002, sql);
        }
        // 无异常时,打印执行成功信息
        cout << "[SQL模块] 执行成功,SQL:" << sql << endl;
    }
};

// 缓存模块:依赖SQL模块,模拟缓存读写,随机触发异常
class CacheMgr {
public:
    // 构造函数:通过智能指针注入SQL模块(依赖注入,降低耦合)
    CacheMgr(shared_ptr<SqlMgr> sql_mgr)
        : _sql_mgr(sql_mgr)
    {}

    // 模拟缓存获取:1/5概率触发"缓存不存在",1/6概率触发"缓存过期"
    void get(const string& key) {
        int rand_val1 = rand() % 5;  // 生成0-4的随机数(判断缓存是否存在)
        if (rand_val1 == 0) {
            // 抛出"缓存不存在"异常(错误ID:2001),并调用SQL模块加载数据
            cout << "[缓存模块] 缓存键" << key << "不存在,尝试从SQL加载..." << endl;
            _sql_mgr->query("SELECT * FROM cache_data WHERE key = '" + key + "'");
            throw CacheException("缓存不存在:已触发SQL加载", 2001, key);
        }

        int rand_val2 = rand() % 6;  // 生成0-5的随机数(判断缓存是否过期)
        if (rand_val2 == 0) {
            // 抛出"缓存过期"异常(错误ID:2002),并调用SQL模块更新缓存
            cout << "[缓存模块] 缓存键" << key << "已过期,尝试从SQL更新..." << endl;
            _sql_mgr->query("UPDATE cache_data SET value = new_val WHERE key = '" + key + "'");
            throw CacheException("缓存过期:已触发SQL更新", 2002, key);
        }

        // 无异常时,打印缓存获取成功信息
        cout << "[缓存模块] 获取成功,缓存键:" << key << endl;
    }

private:
    shared_ptr<SqlMgr> _sql_mgr;  // 智能指针管理SQL模块,自动释放资源
};

// HTTP模块:依赖缓存模块,模拟HTTP请求处理,随机触发异常
class HttpServer {
public:
    // 构造函数:通过智能指针注入缓存模块(依赖注入,降低耦合)
    HttpServer(shared_ptr<CacheMgr> cache_mgr)
        : _cache_mgr(cache_mgr)
    {}

    // 模拟HTTP请求处理:1/3概率触发"资源不存在",1/4概率触发"权限不足"
    void handle_request(const string& method, const string& path) {
        // 模拟请求校验:先检查请求路径是否合法
        if (path != "/user/info" && path != "/order/list" && path != "/goods/detail") {
            // 抛出"请求资源不存在"异常(错误ID:3001)
            throw HttpException("请求资源不存在:路径未注册", 3001, method, path);
        }

        int rand_val = rand() % 4;  // 生成0-3的随机数(判断权限)
        if (rand_val == 0 && method == "POST") {
            // 抛出"POST请求权限不足"异常(错误ID:3002)
            throw HttpException("权限不足:需要登录才能执行POST请求", 3002, method, path);
        }

        // 无异常时,调用缓存模块获取数据
        cout << "[HTTP模块] 接收请求:" << method << " " << path << ",尝试获取缓存..." << endl;
        _cache_mgr->get(path);  // 调用缓存模块,可能触发缓存/SQL异常
    }

private:
    shared_ptr<CacheMgr> _cache_mgr;  // 智能指针管理缓存模块,自动释放资源
};

// -------------------------- 3. 主函数(统一异常捕获+服务循环) --------------------------
int main() {
    // 1. 初始化随机数种子(确保每次运行异常触发概率随机)
    srand(time(0));

    // 2. 初始化模块(智能指针管理,确保异常安全,避免资源泄漏)
    shared_ptr<SqlMgr> sql_mgr = make_shared<SqlMgr>();
    shared_ptr<CacheMgr> cache_mgr = make_shared<CacheMgr>(sql_mgr);
    shared_ptr<HttpServer> http_server = make_shared<HttpServer>(cache_mgr);

    // 3. 模拟服务循环:每秒处理一次请求,持续运行
    cout << "=== 服务启动成功,开始处理请求(每秒1次)===" << endl;
    while (1) {
        // 模拟随机HTTP请求(方法:GET/POST,路径:3个合法路径中随机选)
        string method = (rand() % 2 == 0) ? "GET" : "POST";
        string paths[] = {"/user/info", "/order/list", "/goods/detail", "/invalid/path"};
        string path = paths[rand() % 4];  // 1/4概率选择非法路径

        try {
            // 处理HTTP请求:可能触发HTTP、缓存、SQL模块的异常
            http_server->handle_request(method, path);
        } 
        // 4. 捕获所有自定义异常(基类引用,多态匹配所有子类异常)
        catch (const Exception& e) {
            cout << "=== 捕获到异常 ===" << endl;
            cout << e.what() << endl;  // 多态调用子类的what()方法
        }
        // 5. 捕获未知异常(兜底处理,避免程序终止)
        catch (...) {
            cout << "=== 捕获到未知异常 ===" << endl;
            cout << "错误描述:未知异常类型,可能是系统级错误" << endl;
        }

        // 6. 服务周期:每秒处理一次请求
        cout << "=== 本次请求处理结束,等待1秒..." << endl << endl;
        this_thread::sleep_for(chrono::seconds(1));
    }

    return 0;
}

9.3 代码解析与运行效果

9.3.1 核心设计亮点(贴合异常处理规范)
  1. 异常体系解耦:通过基类Exception统一异常类型,子类仅需补充模块专属信息(如 SQL 语句、缓存键),符合 “派生类到基类转换” 的异常匹配规则,实现 “一 catch 多处理”;
  2. 异常安全保障:使用shared_ptr管理模块对象,无需手动new/delete,避免异常抛出时资源泄漏;所有析构函数声明为noexcept,符合 “别让异常逃离析构函数” 的规范;
  3. 错误信息完整:异常包含时间戳、错误 ID、模块类型、业务上下文(如 SQL 语句、请求路径),便于问题定位,解决了 C 语言错误码 “信息模糊” 的痛点;
  4. 服务稳定性main函数中while(1)循环 +catch(...)兜底,确保即使触发未知异常,程序也不会终止,符合服务端 “持续运行” 的需求。
9.3.2 典型运行效果(随机触发异常)

从运行效果可见:

  • 无异常时,各模块正常调用(HTTP→缓存→SQL);
  • 触发异常时,异常沿调用链向上传播(如 SQL 异常→缓存异常→HTTP 异常),最终被main函数的catch(const Exception& e)统一捕获;
  • 异常信息包含时间、模块、业务上下文,定位问题清晰;
  • 程序持续运行,即使触发异常也不会终止,符合服务端稳定性要求。

9.4 实战扩展:异常重试机制

基于上述代码,我们可以增加 “异常重试” 功能 —— 针对 “临时错误”(如缓存过期、网络波动),重试 3 次后再抛出异常;针对 “永久错误”(如权限不足、资源不存在),直接抛出异常。

扩展后的 HTTP 模块handle_request方法
void handle_request(const string& method, const string& path) {
    const int max_retry = 3;  // 最大重试次数
    int retry_count = 0;       // 当前重试次数

    while (retry_count < max_retry) {
        try {
            // 1. 校验请求路径(永久错误,不重试)
            if (path != "/user/info" && path != "/order/list" && path != "/goods/detail") {
                throw HttpException("请求资源不存在:路径未注册", 3001, method, path);
            }

            // 2. 校验POST请求权限(永久错误,不重试)
            if (method == "POST" && rand() % 4 == 0) {
                throw HttpException("权限不足:需要登录才能执行POST请求", 3002, method, path);
            }

            // 3. 调用缓存模块(临时错误,可重试)
            cout << "[HTTP模块] 接收请求:" << method << " " << path << ",尝试获取缓存(第" << retry_count + 1 << "次)..." << endl;
            _cache_mgr->get(path);
            return;  // 无异常,直接返回
        }
        // 4. 捕获临时错误(缓存过期/不存在,重试)
        catch (const CacheException& e) {
            if (e.getid() == 2001 || e.getid() == 2002) {  // 2001=缓存不存在,2002=缓存过期
                retry_count++;
                if (retry_count >= max_retry) {
                    // 重试次数用尽,抛出最终异常
                    throw HttpException("请求失败:缓存重试" + to_string(max_retry) + "次后仍失败", 3003, method, path);
                }
                cout << "[HTTP模块] 临时错误," << max_retry - retry_count << "次重试机会,等待0.5秒..." << endl;
                this_thread::sleep_for(chrono::milliseconds(500));  // 重试间隔0.5秒
            } else {
                // 其他缓存错误(非临时),直接抛出
                throw;
            }
        }
        // 5. 捕获永久错误(直接抛出,不重试)
        catch (const Exception& e) {
            throw;
        }
    }
}
扩展后运行效果(重试机制生效)
=== 本次请求处理开始 ===
[HTTP模块] 接收请求:GET /order/list,尝试获取缓存(第1次)...
[缓存模块] 缓存键/order/list已过期,尝试从SQL更新...
[SQL模块] 执行成功,SQL:UPDATE cache_data SET value = new_val WHERE key = '/order/list'
[HTTP模块] 临时错误,2次重试机会,等待0.5秒...
[HTTP模块] 接收请求:GET /order/list,尝试获取缓存(第2次)...
[缓存模块] 缓存键/order/list已过期,尝试从SQL更新...
[SQL模块] 执行成功,SQL:UPDATE cache_data SET value = new_val WHERE key = '/order/list'
[HTTP模块] 临时错误,1次重试机会,等待0.5秒...
[HTTP模块] 接收请求:GET /order/list,尝试获取缓存(第3次)...
[缓存模块] 获取成功,缓存键:/order/list
=== 本次请求处理结束,等待1秒...

=== 本次请求处理开始 ===
[HTTP模块] 接收请求:GET /goods/detail,尝试获取缓存(第1次)...
[缓存模块] 缓存键/goods/detail不存在,尝试从SQL加载...
[SQL模块] 执行成功,SQL:SELECT * FROM cache_data WHERE key = '/goods/detail'
[HTTP模块] 临时错误,2次重试机会,等待0.5秒...
[HTTP模块] 接收请求:GET /goods/detail,尝试获取缓存(第2次)...
[缓存模块] 缓存键/goods/detail不存在,尝试从SQL加载...
[SQL模块] 执行成功,SQL:SELECT * FROM cache_data WHERE key = '/goods/detail'
[HTTP模块] 临时错误,1次重试机会,等待0.5秒...
[HTTP模块] 接收请求:GET /goods/detail,尝试获取缓存(第3次)...
[缓存模块] 缓存键/goods/detail不存在,尝试从SQL加载...
[SQL模块] 执行成功,SQL:SELECT * FROM cache_data WHERE key = '/goods/detail'
=== 捕获到异常 ===
HttpException:[2024-05-20 15:35:10] 错误ID:3003,描述:请求失败:缓存重试3次后仍失败,请求方法:GET,路径:/goods/detail
=== 本次请求处理结束,等待1秒...

重试机制的核心是 “基于错误 ID 分类处理”—— 通过e.getid()判断异常是否为 “临时错误”,仅对临时错误重试,避免对永久错误做无效重试,既提升了服务成功率,又保证了处理效率。

十、C++ 异常总结

通过前面的学习,我们从基础概念到实战落地,全面掌握了 C++ 异常处理的核心知识。这里我们对关键内容进行梳理,形成完整的知识体系。

10.1 异常的核心价值

  • 解耦错误检测与处理:异常允许 “检测错误的模块” 和 “处理错误的模块” 独立开发,检测模块只需抛出异常,无需关心处理逻辑;
  • 错误信息丰富:异常以对象形式存在,可携带时间戳、错误 ID、业务上下文(如 SQL 语句、请求路径),远超 C 语言错误码的信息承载能力;
  • 强制错误传播:未捕获的异常会沿调用链自动传播,避免 “错误被忽略” 导致的隐性 bug;
  • 保障服务稳定性:通过catch(...)兜底和重试机制,可确保程序即使触发异常也不终止,符合服务端持续运行的需求。

10.2 异常使用的关键规则

  1. 抛出规则

    • 抛出的异常对象建议是 “拷贝语义安全” 的(避免抛出局部对象的引用);
    • 优先抛出自定义异常(继承自Exception或标准库std::exception),避免抛出内置类型(如intconst char*),便于统一处理;
    • 重新抛出时使用throw;,而非throw e;,避免 “切片问题” 导致子类信息丢失。
  2. 捕获规则

    • catch的顺序是 “子类在前,父类在后”,避免父类catch屏蔽子类catch
    • 优先使用 “引用捕获”(const Exception& e),避免异常对象拷贝,同时支持多态;
    • 必须在main函数中添加catch(...)兜底,避免未知异常导致程序终止。
  3. 异常安全规则

    • 用 RAII 机制(如智能指针、自定义资源管理类)管理资源,避免异常抛出时内存 / 文件句柄泄漏;
    • 析构函数必须声明为noexcept,禁止异常逃离析构函数;
    • 避免在构造函数中做复杂操作,若必须做,需在try-catch中处理异常,避免构造出 “半初始化对象”。
  4. 异常规范规则

    • C++11 及以后优先使用noexcept声明 “不会抛出异常的函数”,帮助编译器优化,同时明确函数行为;
    • 不滥用noexcept:若函数内部可能抛出异常(或调用可能抛出异常的函数),禁止声明noexcept,否则运行时会调用terminate()终止程序;
    • 团队开发中,需明确函数的异常规范(是否抛出、抛出哪些类型),避免调用者因信息不足导致未处理异常。

10.3 异常的适用场景与局限性

适用场景:
  • 服务端程序:需持续运行,异常可确保程序不终止,配合重试机制提升成功率;
  • 复杂业务系统:模块多、依赖关系复杂,异常可解耦错误处理逻辑,便于维护;
  • 标准库 / 框架开发:如智能指针、容器(vectorstring),异常可传递 “内存不足”“索引越界” 等错误,便于上层处理。
局限性:
  • 性能开销:异常处理会生成额外的栈展开代码,若在 “高频调用的函数”(如循环内的计算函数)中频繁抛出异常,会影响性能;
  • 调试难度:异常沿调用链传播,若未正确记录上下文,定位异常根源会比错误码更复杂;
  • 兼容性问题:C 语言不支持异常,若 C++ 代码需调用 C 语言库,需在接口层将 C 语言错误码转换为 C++ 异常,增加适配成本。

10.4 最佳实践建议

  1. 设计清晰的异常体系

    • 定义一个顶层基类Exception,包含通用错误信息(时间戳、错误 ID);
    • 按模块 / 业务场景设计子类(如SqlExceptionHttpException),补充专属信息;
    • 子类重写what()方法,统一错误信息格式,便于日志输出和问题定位。
  2. 合理使用异常与错误码

    • 对 “运行时意外错误”(如内存不足、网络波动)使用异常;
    • 对 “编译时可检测的逻辑错误”(如参数为空、索引越界)使用断言(assert)或返回错误码;
    • 对 “高频调用的简单函数”(如工具类函数),优先使用错误码,避免异常的性能开销。
  3. 完善异常日志

    • catch块中记录异常的完整信息(e.what()),包括时间戳、调用栈、业务参数;
    • 对关键业务(如支付、订单),需将异常日志同步到监控系统,实时告警,快速响应。
  4. 持续测试异常场景

    • 编写单元测试时,需覆盖 “异常触发场景”(如模拟除零、内存不足、权限不足);
    • 进行压力测试时,需模拟 “高异常率场景”,验证程序的稳定性和资源泄漏情况。

C++ 异常是一把 “双刃剑”—— 用得好可以大幅提升代码的可维护性和服务稳定性,用得不好则会引入性能问题和隐性 bug。只有深入理解其核心机制,遵循最佳实践,才能让异常成为开发中的 “助力”,而非 “负担”。


网站公告

今日签到

点亮在社区的每一天
去签到