C++ 异常处理库 <exception>(长文解析)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

异常处理的基本概念与核心语法

在 C++ 程序设计中,异常处理是一种用于管理程序运行时错误的机制。它允许开发者通过 try-catch 块将代码划分为“尝试执行”和“错误处理”两个部分,从而避免程序因未处理的错误而崩溃。而 <exception> 库正是实现这一功能的核心工具箱。

try-catch 块:程序的“安全气囊”

try 块包裹着可能抛出异常的代码,而 catch 块则用于捕获并处理这些异常。例如:

#include <iostream>  
#include <exception>  

int main() {  
    try {  
        int numerator = 10;  
        int denominator = 0;  
        if (denominator == 0) {  
            throw std::exception(); // 抛出异常对象  
        }  
        std::cout << "Result: " << numerator / denominator << std::endl;  
    } catch (const std::exception& e) {  
        std::cerr << "Error: Division by zero occurred." << std::endl;  
    }  
    return 0;  
}  

在这个示例中,当分母为 0 时,程序会抛出一个 std::exception 对象,并跳转到最近的 catch 块进行处理。这种机制类似于交通信号灯:当检测到危险(异常)时,程序立即切换到安全路径(catch 块),避免进一步的“事故”。

throw 表达式:异常的“触发器”

throw 关键字用于显式抛出异常对象。开发者可以抛出任意类型的对象,但通常推荐使用 <exception> 库中的标准异常类(如 std::runtime_errorstd::invalid_argument),因为它们提供了统一的错误信息接口。例如:

void divide(int a, int b) {  
    if (b == 0) {  
        throw std::runtime_error("Division by zero!"); // 抛出具体异常  
    }  
    std::cout << "Result: " << a / b << std::endl;  
}  

<exception> 库的核心函数详解

除了基础语法,<exception> 库还提供了多个全局函数,用于控制异常的处理流程和行为。

std::terminate():程序的“紧急制动”

当以下情况发生时,std::terminate() 会被自动调用:

  • 没有 catch 块匹配抛出的异常类型。
  • catch 块中再次抛出异常(throw;)。
  • 调用了 std::terminate() 函数本身。

默认情况下,std::terminate() 会终止程序并调用 std::abort(),但开发者可以通过 std::set_terminate() 自定义终止行为。例如:

#include <exception>  

void custom_terminate() {  
    std::cerr << "Custom termination handler called." << std::endl;  
    exit(EXIT_FAILURE);  
}  

int main() {  
    std::set_terminate(custom_terminate); // 设置自定义终止函数  
    throw 42; // 未被捕获的异常将触发 custom_terminate()  
    return 0;  
}  

std::unexpected():未被捕获异常的“缓冲区”

当抛出的异常类型与 catch 块声明的类型不匹配时,std::unexpected() 会被调用。默认行为是直接触发 std::terminate(),但可以通过 std::set_unexpected() 改变这一行为。例如:

#include <exception>  

void custom_unexpected() {  
    std::cerr << "Unexpected exception type encountered." << std::endl;  
    std::terminate(); // 必须最终调用 std::terminate()  
}  

int main() {  
    std::set_unexpected(custom_unexpected);  
    try {  
        throw "An unexpected string exception"; // 未声明的类型  
    } catch (int e) {  
        std::cerr << "Caught an int exception." << std::endl;  
    }  
    return 0;  
}  

异常规范与 std::uncaught_exceptions()

C++17 引入的 std::uncaught_exceptions() 函数可以返回当前未被捕获的异常数量,这对复杂嵌套的异常处理场景非常有用。例如:

void nested_function() {  
    try {  
        throw std::exception(); // 抛出第一个异常  
        std::cout << "This line won't execute." << std::endl;  
    } catch (...) {  
        std::cout << "Caught first exception." << std::endl;  
        throw; // 重新抛出,形成嵌套  
    }  
}  

int main() {  
    try {  
        nested_function();  
    } catch (...) {  
        std::cout << "Number of uncaught exceptions: "  
                  << std::uncaught_exceptions() << std::endl; // 输出 0  
    }  
    return 0;  
}  

实际案例:文件读写与资源管理

异常处理库的真正价值在于处理复杂场景中的资源管理和错误传播。例如,一个文件读写函数可能需要处理以下问题:

  1. 文件无法打开。
  2. 读取过程中发生 I/O 错误。
  3. 内存分配失败。
#include <fstream>  
#include <exception>  
#include <memory>  

class FileHandler {  
public:  
    FileHandler(const std::string& filename)  
        : file{std::ifstream{filename}} {  
        if (!file.is_open()) {  
            throw std::runtime_error("Failed to open file.");  
        }  
    }  

    ~FileHandler() {  
        if (file.is_open()) {  
            file.close();  
        }  
    }  

    void read_data() {  
        std::string buffer;  
        while (std::getline(file, buffer)) {  
            if (buffer.empty()) {  
                throw std::invalid_argument("Empty line detected.");  
            }  
            process_data(buffer); // 假设存在此函数  
        }  
    }  

private:  
    std::ifstream file;  
};  

int main() {  
    try {  
        FileHandler fh{"data.txt"};  
        fh.read_data();  
    } catch (const std::exception& e) {  
        std::cerr << "Error: " << e.what() << std::endl;  
        return EXIT_FAILURE;  
    }  
    return 0;  
}  

在这个案例中,FileHandler 类通过构造函数抛出异常,并在 read_data 方法中检测数据问题。这种设计确保了资源(文件流)的正确释放,并将错误信息传递给调用者。

注意事项与最佳实践

异常规范的“隐形契约”

C++ 允许函数声明 throw 子句(如 void func() throw(int)),但这一功能在 C++11 中被弃用。现代代码应依赖类型安全的 try-catch 设计,而非显式声明异常类型。

资源管理与 RAII

在抛出异常时,确保资源(如内存、文件句柄)被正确释放至关重要。C++ 的 RAII(资源获取即初始化)模式通过对象的生命周期管理资源,例如:

class Resource {  
public:  
    Resource() {  
        ptr = new int[100]; // 分配资源  
    }  

    ~Resource() {  
        delete[] ptr; // 确保释放资源,无论是否抛出异常  
    }  

private:  
    int* ptr;  
};  

void risky_function() {  
    Resource res; // 构造时分配资源  
    if (/* 检测到错误 */) {  
        throw std::runtime_error("Error occurred"); // 资源仍会被释放  
    }  
}  

避免在 catch 块中隐藏错误

catch(...) 会捕获所有异常类型,但直接忽略错误信息可能导致难以调试的问题。例如:

try {  
    // 可能抛出多种异常的代码  
} catch (...) {  
    std::cerr << "An error occurred." << std::endl; // 缺乏具体信息  
}  

更好的做法是捕获具体异常类型,并记录详细信息:

catch (const std::runtime_error& e) {  
    log_error(e.what()); // 记录具体错误信息  
}  

结论

<exception> 库是 C++ 异常处理机制的核心,它通过 try-catch 块、标准异常类以及全局函数,为开发者提供了一套灵活且强大的错误处理工具。无论是简单的算术错误还是复杂的资源管理场景,合理使用这些功能都能显著提升程序的健壮性和可维护性。

对于初学者,建议从基础语法开始,逐步掌握异常传播、资源管理和自定义终止函数的使用;中级开发者则可以深入探索 <exception> 库的底层实现细节,并结合 RAII 模式优化代码结构。记住,异常处理不是“错误发生的终点”,而是程序自我修复和容错能力的起点。

通过本文的讲解,希望读者能够建立起对 C++ 异常处理库的系统性认知,并在实际开发中善用这些工具,让代码在复杂场景下依然优雅运行。

最新发布