C++异常使用 | Kai@Codehubble

引言

在C++学习初期,或者在C语言中,我们都是使用返回值处理错误--比如用-1表示函数执行失败,用nullptr表示资源获取失败。这种方式直观简单,在简单代码中完全够用。但当项目变大、函数调用层级变深(比如 “接口层 -> 服务层 -> 数据层” 的多层调用)时,返回值的短板会暴露无遗,代码会被大量 “错误码传递” 逻辑淹没。而 C++ 异常机制,正是为解决复杂场景下的错误处理痛点而生。

本文将从新手最熟悉的返回值入手,通过真实的深层调用场景暴露其局限性,再对比异常的解决方案,逐步拆解 C++ 异常体系、基础用法和关键注意事项,帮助你建立 “场景化选择错误处理方式” 的思维。

返回值 vs 异常:深层调用场景下的鲜明对比

很多新手觉得 “返回值够用”,是因为没遇到复杂的多层调用场景。下面通过一个 “文件数据处理” 的真实案例,直观感受返回值的局限性,以及异常如何解决这些问题。

场景说明

假设我们需要实现一个 “读取配置文件 -> 解析数据 -> 计算结果” 的流程,分为三层函数调用:

  • 底层:readConfigFile(读取文件,返回数据字符串);
  • 中层:parseConfigData(解析数据字符串,返回配置对象);
  • 上层:calculateResult(使用配置对象计算,返回结果);
  • 入口:main函数调用上层函数,处理结果或错误。

用返回值(错误码)实现:冗余的 “错误传递链”

为了表示错误,我们先定义错误码枚举,再让每个函数返回 “错误码 + 结果”(用结构体封装,避免返回值语义污染):

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

// 定义错误码
enum class ErrorCode {
    SUCCESS = 0,
    FILE_OPEN_FAILED,   // 文件打开失败
    FILE_READ_FAILED,   // 文件读取失败
    DATA_PARSE_FAILED,  // 数据解析失败
    CONFIG_INVALID      // 配置无效
};

// 配置数据结构
struct Config {
    int threshold;
    double factor;
};

// 底层:读取配置文件,返回“错误码+数据字符串”
struct ReadResult {
    ErrorCode err;
    string data;
};
ReadResult readConfigFile(const string& path) {
    FILE* fp = fopen(path.c_str(), "r");
    // 错误1:文件打开失败
    if (!fp) {
        return {ErrorCode::FILE_OPEN_FAILED, ""};
    }

    char buf[1024] = {0};
    size_t readLen = fread(buf, 1, sizeof(buf)-1, fp);
    // 错误2:文件读取失败
    if (readLen == 0) {
        fclose(fp);
        return {ErrorCode::FILE_READ_FAILED, ""};
    }

    fclose(fp);
    return {ErrorCode::SUCCESS, string(buf)};
}

// 中层:解析数据字符串,返回“错误码+配置对象”
struct ParseResult {
    ErrorCode err;
    Config config;
};
ParseResult parseConfigData(const string& data) {
    // 模拟解析逻辑:假设数据格式是“threshold:factor”
    size_t colonPos = data.find(':');
    // 错误3:数据格式错误,解析失败
    if (colonPos == string::npos) {
        return {ErrorCode::DATA_PARSE_FAILED, {}};
    }

    Config cfg;
    try {
        cfg.threshold = stoi(data.substr(0, colonPos));
        cfg.factor = stod(data.substr(colonPos+1));
    } catch (...) {
        return {ErrorCode::DATA_PARSE_FAILED, {}};
    }

    // 错误4:配置值无效(阈值必须大于0)
    if (cfg.threshold <= 0) {
        return {ErrorCode::CONFIG_INVALID, {}};
    }

    return {ErrorCode::SUCCESS, cfg};
}

// 上层:使用配置计算结果,返回“错误码+结果”
struct CalculateResult {
    ErrorCode err;
    double result;
};
CalculateResult calculateResult(const string& configPath) {
    // 调用底层:读取文件
    ReadResult readRes = readConfigFile(configPath);
    // 必须检查错误,传递给上层
    if (readRes.err != ErrorCode::SUCCESS) {
        return {readRes.err, 0.0};
    }

    // 调用中层:解析数据
    ParseResult parseRes = parseConfigData(readRes.data);
    // 必须检查错误,传递给上层
    if (parseRes.err != ErrorCode::SUCCESS) {
        return {parseRes.err, 0.0};
    }

    // 核心计算逻辑
    Config cfg = parseRes.config;
    double result = cfg.threshold * cfg.factor * 100;
    return {ErrorCode::SUCCESS, result};
}

// 入口函数
int main() {
    CalculateResult calcRes = calculateResult("config.txt");
    // 最终检查错误,处理异常
    if (calcRes.err != ErrorCode::SUCCESS) {
        switch (calcRes.err) {
            case ErrorCode::FILE_OPEN_FAILED:
                cerr << "错误:文件打开失败" << endl;
                break;
            case ErrorCode::FILE_READ_FAILED:
                cerr << "错误:文件读取失败" << endl;
                break;
            case ErrorCode::DATA_PARSE_FAILED:
                cerr << "错误:数据解析失败" << endl;
                break;
            case ErrorCode::CONFIG_INVALID:
                cerr << "错误:配置无效" << endl;
                break;
            default:
                cerr << "错误:未知错误" << endl;
        }
        return 1;
    }

    cout << "计算结果:" << calcRes.result << endl;
    return 0;
}

返回值的致命局限性(深层调用场景下)

看完上面的代码,你会发现 3 个无法回避的问题:

  1. 业务逻辑被错误处理淹没calculateResult函数的核心逻辑只有 1 行(double result = ...),但其余代码全是 “检查错误 -> 传递错误” 的冗余逻辑(if (err != SUCCESS) return err)。随着调用层级增加(比如再加 “网络层”“缓存层”),这种冗余会呈指数级增长;
  2. 返回值语义污染:为了同时返回 “结果 + 错误码”,我们不得不定义ReadResultParseResult等结构体,增加了代码复杂度。如果函数本身返回值类型复杂(比如std::vector<std::map<string, int>>),结构体的定义会更繁琐;
  3. 构造函数无法处理错误:如果Config类的构造函数需要依赖文件数据初始化(比如Config(const string& path)),构造函数没有返回值,无法通过返回值表示初始化失败 —— 这是返回值的 “天生缺陷”;
  4. 错误处理分散:错误处理逻辑分散在每一层函数中,一旦需要修改错误处理方式(比如新增错误类型),所有层级的函数都要修改,维护成本极高。

用异常实现:剥离错误处理,专注业务逻辑

#include <string>
#include <iostream>
#include <stdexcept> // 标准异常头文件
#include <fstream>
using namespace std;

// 配置数据结构
struct Config {
    int threshold;
    double factor;

    // 构造函数:直接接收文件路径,初始化失败则抛异常(解决返回值的天生缺陷)
    Config(const string& path) {
        // 读取文件(用std::ifstream,RAII自动管理资源)
        ifstream file(path);
        if (!file.is_open()) {
            throw runtime_error("文件打开失败"); // 抛出异常,无需返回值
        }

        string data;
        if (!getline(file, data)) {
            throw runtime_error("文件读取失败");
        }

        // 解析数据
        size_t colonPos = data.find(':');
        if (colonPos == string::npos) {
            throw invalid_argument("数据解析失败:格式错误");
        }

        try {
            threshold = stoi(data.substr(0, colonPos));
            factor = stod(data.substr(colonPos+1));
        } catch (...) {
            throw invalid_argument("数据解析失败:数值无效");
        }

        // 校验配置
        if (threshold <= 0) {
            throw logic_error("配置无效:阈值必须大于0");
        }
    }
};

// 上层:专注核心计算,无需处理错误传递
double calculateResult(const string& configPath) {
    Config cfg(configPath); // 构造失败会抛异常,直接跳过后续代码
    return cfg.threshold * cfg.factor * 100; // 核心业务逻辑
}

// 入口函数:集中处理所有异常
int main() {
    try {
        double result = calculateResult("config.txt");
        cout << "计算结果:" << result << endl;
    }
    // 集中捕获不同类型的异常,统一处理
    catch (const runtime_error& e) {
        cerr << "运行时错误:" << e.what() << endl;
    }
    catch (const invalid_argument& e) {
        cerr << "参数错误:" << e.what() << endl;
    }
    catch (const logic_error& e) {
        cerr << "逻辑错误:" << e.what() << endl;
    }
    catch (const exception& e) {
        cerr << "未知异常:" << e.what() << endl;
    }

    return 0;
}

异常的核心优势(对比返回值)

对比两段代码,异常的优势一目了然:

  1. 剥离错误处理与业务逻辑:中间层(calculateResult)无需关心错误传递,只专注核心业务逻辑,代码量减少 60% 以上;
  2. 支持无返回值场景:构造函数、运算符等无返回值的场景,可通过异常表示错误(如Config构造函数),弥补了返回值的天生缺陷;
  3. 错误处理集中化:所有错误都在入口函数的catch块集中处理,修改错误类型或处理逻辑时,只需修改一处,维护成本低;
  4. 错误信息更丰富:异常对象可携带详细错误信息(如what()返回的描述),比单纯的错误码更易调试。

场景化选择:什么时候用返回值,什么时候用异常?

通过上面的对比,我们可以明确二者的适用场景 —— 它们是互补关系,而非替代关系:

场景类型 推荐方案 核心原因
简单参数校验、查询无结果 返回值(优先用std::optional 错误可预期、非致命,无需中断流程
非致命、可忽略的错误 返回值(错误码 /std::expected 如日志写入失败、缓存未命中,不影响主流程
致命错误、必须处理的错误 异常 如文件打开失败、内存分配失败,需中断当前流程
构造函数 / 运算符错误 异常(唯一选择) 无返回值,无法用返回值表示错误
深层调用场景(多层函数调用) 异常 避免冗余的错误码传递,简化代码

小贴士:C++17 的std::optional(表示 “可能无结果”)和 C++23 的std::expected(表示 “结果或错误”)是返回值处理的 “升级方案”,比传统错误码更优雅,推荐在简单场景下使用。

异常基本写法

C++ 异常依赖三个关键字,构成 “抛出 - 监控 - 捕获” 的闭环:

  • 抛出(throw:当程序检测到错误时,用throw语句抛出一个 “异常对象”(可以是标准异常、自定义异常),直接中断当前代码流;
  • 监控(try:用try块包裹 “可能抛出异常的代码”,告诉编译器 “这里需要监控异常”;
  • 捕获(catchtry块后紧跟一个或多个catch块,每个catch指定要捕获的异常类型,匹配成功则执行对应的错误处理逻辑。
try
{
  // 保护的标识代码
}
 catch( ExceptionName &e1 )
{
  // catch 块
}
 catch( ExceptionName &e2 )
{
  // catch 块
}
 catch( ExceptionName &eN )
{
  // catch 块
}
 catch( ... ) //捕获任意类型异常,防止某个异常直到程序结束都没被捕获
{
  // catch 块
  cout << "Unkown Exception" << endl;
}

异常类型

异常对象可以是内置类型(如throw -1throw "error"),但可读性差、无法携带详细信息,不推荐。实际开发中,异常通常是 “类对象”,分为标准异常体系和自定义异常。

标准异常体系

C++ 标准库提供了一套现成的异常基类std::exception(定义在<exception>头文件中),所有标准异常都是它的派生类,核心成员是what()方法(返回 C 风格字符串,描述异常信息)。

@startuml C++ 标准异常体系(规整版)
' 布局方向:自上而下,垂直分层
top to bottom direction

' 第1层:顶层根类(居中置顶)
class std::exception #lightblue

' 第2层:核心分支(居中)+ 独立异常(左右分组,缩减宽度)
' 左组独立异常
class std::bad_alloc #lightyellow
class std::bad_cast #lightyellow
' 中组核心分支
class std::logic_error #lightgreen
class std::runtime_error #lightgreen
' 右组独立异常
class std::bad_exception #lightyellow
class std::bad_typeid #lightyellow
class std::system_error #lightyellow

' 第3层:补充独立异常(下一层,避免同层拥挤)
class std::bad_function_call #lightyellow
class std::bad_weak_ptr #lightyellow

' 第4层:logic_error 子类(垂直对齐父类)
class std::invalid_argument #e6f7ff
class std::out_of_range #e6f7ff
class std::length_error #e6f7ff
class std::domain_error #e6f7ff

' 第5层:runtime_error 子类(垂直对齐父类)
class std::overflow_error #e6f7ee
class std::underflow_error #e6f7ee
class std::range_error #e6f7ee

' 继承关系(箭头无交叉,层级清晰)
std::exception <|-- std::bad_alloc
std::exception <|-- std::bad_cast
std::exception <|-- std::logic_error
std::exception <|-- std::runtime_error
std::exception <|-- std::bad_exception
std::exception <|-- std::bad_typeid
std::exception <|-- std::system_error
std::exception <|-- std::bad_function_call
std::exception <|-- std::bad_weak_ptr

std::logic_error <|-- std::domain_error
std::logic_error <|-- std::invalid_argument
std::logic_error <|-- std::length_error
std::logic_error <|-- std::out_of_range

std::runtime_error <|-- std::range_error
std::runtime_error <|-- std::overflow_error
std::runtime_error <|-- std::underflow_error

' 隐藏冗余信息,聚焦类名+继承关系
hide members
hide methods
@enduml

image-20251102213758434

异常类名 父类 核心描述(新手易懂)
std::exception -(顶层基类) 所有标准异常的根类,提供 what() 方法返回异常描述
std::logic_error std::exception 逻辑错误(编码阶段可规避),如参数无效、越界
std::runtime_error std::exception 运行时错误(运行阶段触发,不可提前预知)
std::bad_alloc std::exception 内存分配失败(new/new[] 申请内存时抛出)
std::bad_cast std::exception 类型转换失败(如 dynamic_cast 向下转换无效)
std::bad_exception std::exception 异常处理链错误(用于异常规范兼容)
std::bad_typeid std::exception 类型识别失败(如 typeid 作用于空指针)
std::bad_function_call std::exception 空函数对象调用(如 std::function 未绑定函数)
std::bad_weak_ptr std::exception std::weak_ptr 锁失败(如指向的对象已销毁)
std::system_error std::exception 系统调用错误(如文件打开失败、网络连接错误)
std::invalid_argument std::logic_error 无效参数(如 std::stoi("abc") 字符串转整数)
std::out_of_range std::logic_error 超出有效范围(如 vector::at(100) 索引越界)
std::length_error std::logic_error 长度错误(如 std::string 构造时长度溢出)
std::domain_error std::logic_error 定义域错误(如不支持的数学运算,如负数开根号)
std::overflow_error std::runtime_error 数值溢出(如 int 运算结果超出最大值)
std::underflow_error std::runtime_error 数值下溢(如浮点数运算精度丢失,结果接近 0)
std::range_error std::runtime_error 范围错误(如计算结果超出有效取值范围)

自定义异常

当标准异常无法满足业务需求时(如需要携带错误码、业务标识),可自定义异常类 ——必须继承std::exception或其派生类,并重写what()方法。

#include <stdexcept>
#include <string>

// 自定义业务异常:继承std::runtime_error
class BusinessError : public std::runtime_error {
private:
    int err_code_; // 业务错误码
public:
    // 构造函数:接收错误码和错误描述
    BusinessError(int err_code, const std::string& msg)
        : std::runtime_error(msg), err_code_(err_code) {}

    // 获取业务错误码
    int getErrorCode() const { return err_code_; }

    // 重写what(),返回更详细的错误信息(可选)
    const char* what() const noexcept override {
        static std::string full_msg = "业务错误[" + std::to_string(err_code_) + "]: " + std::runtime_error::what();
        return full_msg.c_str();
    }
};

// 使用自定义异常
void createOrder(int userId) {
    if (userId <= 0) {
        throw BusinessError(1001, "用户ID无效"); // 抛出自定义异常
    }
}

万能异常捕获

在 C++ 异常处理中,catch(...) 是一种 “万能异常捕获” 语法,也叫 “捕获所有异常” 或 “省略号捕获”—— 它能捕获任何类型的异常—— 它能捕获任何类型的异常(包括标准异常、自定义异常、甚至内置类型如 int/const char* 抛出的异常),是异常捕获体系中的 “终极兜底方案”。

语法定义

try {
    // 可能抛出任意类型异常的代码
}
// 特定异常捕获(先匹配具体类型)
catch (const std::runtime_error& e) {
    // 处理运行时异常
}
catch (const std::logic_error& e) {
    // 处理逻辑异常
}
// 万能捕获(最后匹配,捕获所有未被前面捕获的异常)
catch (...) {
    // 兜底处理所有其他异常
}

核心特点

  • 匹配所有异常:无论 throw 抛出的是 std::exception 派生类、自定义类、intconst char* 等任何类型,catch(...) 都能捕获;
  • 必须放在最后catch 块的匹配顺序是 “自上而下”,catch(...) 会拦截所有异常,因此必须放在所有特定类型 catch 块之后(否则会导致前面的特定 catch 永远无法匹配);
  • 无法直接获取异常信息catch(...) 没有参数,无法访问异常对象的细节(比如 what() 方法返回的描述),只能知道 “有异常发生”。

核心作用:兜底防崩溃

catch(...) 的核心价值是 “防止程序因未捕获的异常直接终止”——C++ 中如果异常未被任何 catch 块捕获,会调用 std::terminate() 终止程序(类似崩溃)。而 catch(...) 能作为 “最后一道防线”,捕获所有遗漏的异常,保证程序能优雅处理(如记录日志、清理资源)后退出,而非直接崩溃。

典型用法示例

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

// 自定义异常
class MyException : public exception {
public:
    const char* what() const noexcept override { return "自定义异常"; }
};

int main() {
    try {
        // 可能抛出多种异常
        throw MyException();      // 自定义异常
        // throw 100;             // 内置类型 int 异常
        // throw "未知错误";       // 内置类型 const char* 异常
        // throw runtime_error("运行时错误"); // 标准异常
    }
    // 特定异常捕获(精准处理已知异常)
    catch (const MyException& e) {
        cout << "捕获到自定义异常:" << e.what() << endl;
    }
    catch (const exception& e) {
        cout << "捕获到标准异常:" << e.what() << endl;
    }
    // 万能捕获(兜底处理所有未匹配的异常)
    catch (...) {
        cout << "捕获到未知类型异常!" << endl;
        // 关键操作:记录日志、清理资源(如释放内存、关闭文件)
        // ... 资源清理逻辑 ...
    }

    return 0;
}

运行结果(抛出 MyException 时):

捕获到自定义异常:自定义异常

若抛出 throw 100(无特定 catch 匹配),运行结果:

捕获到未知类型异常!

注意事项

  1. 必须放在所有 catch 块的最后

错误示例catch(...) 在前,导致特定 catch 失效):

try {
    throw runtime_error("运行时错误");
}
catch (...) { // 放在前面,直接捕获所有异常
    cout << "未知异常" << endl;
}
catch (const exception& e) { // 永远不会执行!
    cout << e.what() << endl;
}

正确示例(特定 catch 在前,catch(...) 兜底):

try {
    throw runtime_error("运行时错误");
}
catch (const exception& e) { // 先匹配特定异常
    cout << e.what() << endl;
}
catch (...) { // 最后兜底
    cout << "未知异常" << endl;
}
  1. 无法获取异常详情,需配合 “重新抛出”

catch(...) 不能访问异常对象,因此无法获取 what() 描述、错误码等信息。如果需要上层处理异常详情,可在 catch(...) 中使用 “重新抛出”(rethrow) 语法 throw;,将异常原样抛给上层:

void func() {
    try {
        throw MyException();
    }
    catch (...) {
        cout << "func 中捕获到异常,记录日志后向上抛出" << endl;
        throw; // 重新抛出异常,不改变异常类型和信息
    }
}

int main() {
    try {
        func();
    }
    catch (const MyException& e) {
        // 上层能获取完整异常信息
        cout << "main 中捕获到异常:" << e.what() << endl;
    }
    return 0;
}

运行结果:

func 中捕获到异常,记录日志后向上抛出
main 中捕获到异常:自定义异常

注意:重新抛出时用 throw;(无参数),而非 throw 异常对象;—— 后者会创建新的异常对象,丢失原始异常信息。

  1. 不可滥用:优先特定捕获,再兜底

catch(...) 是 “兜底方案”,而非首选。原因:

  • 无法精准处理异常(比如不同异常需要不同修复逻辑时,catch(...) 做不到);
  • 可能掩盖潜在 bug(比如捕获了本应暴露的逻辑错误)。

使用原则

  1. 先写所有能预见的 特定类型 catch(如 catch(const std::runtime_error&)catch(const MyException&)),精准处理已知异常;
  2. 最后用 catch(...) 兜底,仅用于:
    • 记录异常日志(便于调试);
    • 清理资源(如释放 RAII 未覆盖的资源);
    • 避免程序直接崩溃,优雅退出或重新抛出异常。

基础用法

结合前面的案例,我们再通过 3 个核心场景,巩固异常的基础用法。

基础用法:抛出 + 捕获标准异常

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

// 除法函数:除数为0时抛异常
double divide(double a, double b) {
    if (b == 0) {
        // 抛出std::invalid_argument异常,携带错误信息
        throw invalid_argument("除数不能为0");
    }
    return a / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl; // 正常执行,输出5
        cout << divide(10, 0) << endl; // 抛出异常,中断执行
    }
    // 捕获特定类型的异常
    catch (const invalid_argument& e) {
        cerr << "捕获到参数异常:" << e.what() << endl; // 输出:捕获到参数异常:除数不能为0
    }
    // 捕获所有std::exception派生的异常(兜底)
    catch (const exception& e) {
        cerr << "捕获到异常:" << e.what() << endl;
    }

    return 0;
}

异常传播:多层调用下的自动传递

如果异常在函数中抛出,但该函数没有catch块,异常会自动 “向上传播” 到调用者,直到找到匹配的catch块:

// 底层函数:抛异常
void funcC() {
    throw runtime_error("funcC中发生运行时错误");
}

// 中层函数:调用funcC,不捕获异常
void funcB() {
    funcC(); // 异常传播到funcB的调用者
}

// 上层函数:调用funcB,不捕获异常
void funcA() {
    funcB(); // 异常传播到funcA的调用者
}

int main() {
    try {
        funcA(); // 异常传播到这里
    }
    catch (const runtime_error& e) {
        cerr << "捕获到传播的异常:" << e.what() << endl; // 输出对应错误信息
    }
    return 0;
}

自定义异常的捕获顺序

catch块的匹配顺序是 “自上而下”,子类异常必须放在父类异常前面,否则子类异常会被父类异常 “拦截”:

int main() {
    try {
        throw BusinessError(1001, "用户ID无效"); // 抛出自定义异常(子类)
    }
    // 正确:先捕获子类异常
    catch (const BusinessError& e) {
        cerr << "业务异常:" << e.what() << ",错误码:" << e.getErrorCode() << endl;
    }
    // 后捕获父类异常(兜底)
    catch (const runtime_error& e) {
        cerr << "运行时异常:" << e.what() << endl;
    }
    catch (const exception& e) {
        cerr << "未知异常:" << e.what() << endl;
    }
    return 0;
}

// 错误示例:父类异常在前,子类异常无法被捕获
try {
    throw BusinessError(1001, "用户ID无效");
}
catch (const runtime_error& e) { // 父类异常在前,直接捕获子类异常
    cerr << "运行时异常:" << e.what() << endl;
}
catch (const BusinessError& e) { // 永远不会执行
    cerr << "业务异常:" << e.what() << endl;
}

使用注意事项

异常虽强大,但使用不当会导致代码崩溃、资源泄漏等严重问题。以下 6 个注意事项,是新手避坑的核心。

异常安全:用 RAII 避免资源泄漏

异常会中断正常代码流,如果手动管理资源(如new/delete、文件句柄、锁),容易导致资源泄漏:

// 反例:异常导致内存泄漏
void badExample() {
    int* arr = new int[10]; // 分配内存
    divide(10, 0); // 抛出异常,后续delete不会执行
    delete[] arr; // 内存泄漏!
}

解决方案:RAII(资源获取即初始化)—— 用对象的生命周期管理资源,析构函数会自动释放资源(即使发生异常)。C++ 标准库的std::unique_ptr(智能指针)、std::lock_guard(锁)、std::ifstream(文件流)都是 RAII 的典型应用:

// 正例:用RAII保证异常安全
#include <memory> // std::unique_ptr

void goodExample() {
    // 智能指针管理内存,异常时自动释放
    unique_ptr<int[]> arr = make_unique<int[]>(10);
    divide(10, 0); // 抛出异常,arr的析构函数自动调用delete[]
}

禁止在析构函数中抛出异常

析构函数的职责是释放资源,若析构函数中抛出异常,会导致严重问题:

  • 如果当前正处于异常传播过程中(已有一个未处理的异常),再抛出新异常,程序会直接调用std::terminate()终止;
  • 析构函数是自动调用的,其异常无法被上层捕获。

规则:析构函数必须是noexcept(C++11 + 默认是noexcept),内部不抛出异常(若有错误,内部处理):

// 正确示例:析构函数不抛出异常
class ResourceHolder {
private:
    FILE* fp_;
public:
    explicit ResourceHolder(const string& path) {
        fp_ = fopen(path.c_str(), "r");
    }

    ~ResourceHolder() noexcept { // 明确noexcept
        if (fp_) {
            try {
                fclose(fp_); // 可能失败的操作
            }
            catch (...) {
                // 内部处理错误,不向外抛出
                cerr << "文件关闭失败" << endl;
            }
        }
    }
};

合理使用noexcept

noexcept用于声明函数 “不会抛出异常”,有两个核心作用:

  • 编译器可优化代码(减少异常处理开销);
  • 若函数违反声明(实际抛出异常),程序会直接调用std::terminate(),避免不可预期的行为。

使用场景:析构函数、移动构造 / 赋值函数、明确无异常的工具函数:

// 声明函数不会抛出异常
int add(int a, int b) noexcept {
    return a + b; // 无异常风险
}

// 移动构造函数声明为noexcept(推荐)
class MyString {
public:
    MyString(MyString&& other) noexcept {
        // 移动资源,无异常
    }
};

不抛出裸指针异常

抛出new分配的异常对象指针(如throw new BusinessError(1001, "错误"))是危险的:

  • 捕获方需要手动delete,容易遗漏导致内存泄漏;
  • 若未被捕获,异常终止时,指针指向的内存永远无法释放。

规则:抛出异常对象(值类型),捕获时用const&(避免拷贝开销):

// 错误:抛出裸指针
throw new BusinessError(1001, "用户ID无效");

// 正确:抛出异常对象
throw BusinessError(1001, "用户ID无效");

// 捕获时用const&
catch (const BusinessError& e) { ... }

避免过度使用异常

异常适合处理 “严重、不可预期” 的错误,不适合处理 “频繁发生的预期错误”(如循环中的参数校验):

// 反例:用异常处理频繁的参数校验
double calculateScore(int score) {
    if (score < 0 || score > 100) {
        throw invalid_argument("分数必须在0-100之间"); // 不推荐,预期内的错误
    }
    return score * 0.8;
}

// 正例:用返回值处理预期错误
std::optional<double> calculateScore(int score) {
    if (score < 0 || score > 100) {
        return std::nullopt; // 预期内的错误,用std::optional表示
    }
    return score * 0.8;
}

异常信息要具体

抛出异常时,携带的错误信息要足够详细(如错误原因、发生位置、相关参数),便于调试:

// 不好:错误信息模糊
throw runtime_error("文件读取失败");

// 好:携带具体信息
throw runtime_error("文件读取失败:路径=" + path + ",错误码=" + to_string(errno));

捕获异常对象时使用引用类型(防止切片问题)

当抛出子类异常对象,但用父类值类型捕获时,会发生 “切片”—— 子类对象中 “父类没有的成员(属性、方法)” 会被 “截断”,最终得到的是一个纯父类对象,丢失子类特有的信息。

这是异常处理中的隐蔽问题,会导致无法获取子类异常的完整错误信息(如自定义异常的业务错误码、详细描述)。

错误示例:

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

// 子类异常:携带业务错误码(父类std::runtime_error没有该成员)
class BusinessError : public runtime_error {
private:
    int err_code_; // 子类特有成员:业务错误码
public:
    BusinessError(int code, const string& msg) 
        : runtime_error(msg), err_code_(code) {}

    // 子类特有方法:获取错误码
    int getErrorCode() const { return err_code_; }

    // 重写what(),拼接错误码和描述
    const char* what() const noexcept override {
        static string full_msg = "错误码[" + to_string(err_code_) + "]: " + runtime_error::what();
        return full_msg.c_str();
    }
};

int main() {
    try {
        // 抛出子类异常对象(含错误码1001)
        throw BusinessError(1001, "用户ID无效");
    }
    // 错误:用父类值类型(runtime_error)捕获
    catch (runtime_error e) { // 这里发生切片!
        // 此时e是纯runtime_error对象,子类的err_code_已被截断
        cout << "捕获到异常:" << e.what() << endl; 
        // 无法调用getErrorCode():e的编译时类型是父类,且子类成员已丢失
        // cout << "错误码:" << e.getErrorCode() << endl; // 编译报错
    }
    return 0;
}

运行结果(切片导致的问题):

捕获到异常:错误码[0]: 用户ID无效  // 子类的err_code_被截断,变成默认值0

C++ 的解决方案:用「父类 const 引用」捕获

C++ 中避免切片的核心是:捕获异常时,永远用「const 引用」类型(而非值类型)。引用传递不会拷贝整个对象,仅绑定到抛出的异常对象,且能完整保留子类的所有成员。

修正后的代码(正确用法):

int main() {
    try {
        throw BusinessError(1001, "用户ID无效");
    }
    // 正确:用父类const引用捕获
    catch (const runtime_error& e) { // 无切片,绑定子类对象
        cout << "捕获到异常:" << e.what() << endl; // 能获取完整子类信息
        // 若需调用子类特有方法,可动态转型(可选)
        if (const BusinessError* be = dynamic_cast<const BusinessError*>(&e)) {
            cout << "错误码:" << be->getErrorCode() << endl;
        }
    }
    return 0;
}

运行结果(无切片):

捕获到异常:错误码[1001]: 用户ID无效
错误码:1001

总结与实践建议

通过本文的案例对比和知识点梳理,你应该明白:C++ 异常不是返回值的替代品,而是复杂场景下的 “优雅解”—— 它解决了深层调用中错误码传递的冗余、无返回值场景的错误处理等痛点。

核心要点回顾

  1. 异常机制:throw抛出异常,try监控代码,catch捕获处理,异常会自动向上传播;
  2. 异常体系:优先使用标准异常,自定义异常继承std::exception,重写what()方法;
  3. 场景选择:简单错误用std::optional/std::expected,严重错误 / 深层调用 / 构造函数错误用异常;
  4. 异常安全:依赖 RAII(智能指针、标准库容器),避免手动管理资源;
  5. 关键禁忌:析构函数不抛异常、捕获顺序子类在前、不抛裸指针异常。

参考

https://cloud.tencent.com/developer/article/2382231

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇