如何在C++中实现自定义异常类并进行异常处理?

在现代 C++ 开发中,异常处理是保证程序健壮性的关键手段。虽然标准库提供了许多现成的异常类型(如 std::runtime_errorstd::logic_error 等),但在实际项目中往往需要根据业务需求创建自己的异常类。本文将从设计原则、继承体系、构造方式以及多态捕获等方面,系统阐述如何在 C++ 中实现并使用自定义异常类。


1. 设计原则

  1. 异常类型要与错误语义相匹配
    每一种异常都应对应一种可辨识的错误状态,方便调用者在 catch 块中精确处理。

  2. 遵循异常安全的 RAII
    异常对象应满足 对象生命周期 的规则,即在抛出时应已经完全构造好,且在被捕获后可以安全析构。

  3. 不要抛弃信息
    异常对象应携带足够的错误信息(如错误码、上下文描述等),以便调试和日志记录。

  4. 异常继承自 std::exceptionstd::runtime_error
    这既符合标准库的异常体系,又能让异常捕获更灵活。


2. 基础实现示例

下面给出一个典型的自定义异常类 FileReadException,用于包装文件读取错误。

#include <exception>
#include <string>
#include <sstream>

class FileReadException : public std::runtime_error {
public:
    explicit FileReadException(const std::string& fileName,
                               const std::string& reason = "unknown")
        : std::runtime_error(buildMessage(fileName, reason)),
          fileName_(fileName), reason_(reason) {}

    const std::string& fileName() const noexcept { return fileName_; }
    const std::string& reason() const noexcept { return reason_; }

private:
    std::string fileName_;
    std::string reason_;

    static std::string buildMessage(const std::string& fileName,
                                    const std::string& reason) {
        std::ostringstream oss;
        oss << "Failed to read file '" << fileName << "': " << reason;
        return oss.str();
    }
};

关键点

  • 继承自 std::runtime_error:既能得到 what() 的默认实现,也可进一步扩展信息。
  • 构造函数中调用 buildMessage:确保 what() 的内容在异常对象内部已完整生成。
  • 提供访问器:允许捕获时进一步获取文件名和原因。

3. 抛出异常

在业务代码中,只要检测到错误,就抛出对应异常:

#include <fstream>

void loadConfig(const std::string& path) {
    std::ifstream fin(path);
    if (!fin) {
        throw FileReadException(path, "cannot open");
    }
    // 读取配置...
}

若想提供更细粒度的错误信息,可以在读取过程中捕获 std::ios_base::failure 并包装:

try {
    // 读取逻辑
}
catch (const std::ios_base::failure& e) {
    throw FileReadException(path, e.what());
}

4. 捕获与处理

4.1 精确捕获

try {
    loadConfig("config.ini");
}
catch (const FileReadException& e) {
    std::cerr << "Error: " << e.what() << '\n';
    std::cerr << "File: " << e.fileName() << '\n';
    std::cerr << "Reason: " << e.reason() << '\n';
}

4.2 多态捕获

若你想统一处理多种自定义异常,可以让它们继承自同一基类:

class AppException : public std::runtime_error {
public:
    explicit AppException(const std::string& msg) : std::runtime_error(msg) {}
};

class FileReadException : public AppException {
    // ...
};

class NetworkException : public AppException {
    // ...
};

然后:

try { /* ... */ }
catch (const AppException& e) {
    std::cerr << "Application error: " << e.what() << '\n';
}

5. 设计更多自定义异常的技巧

场景 推荐做法
错误码 在异常内部维护一个枚举或整数 errorCode_,可通过 int code() const noexcept; 访问。
多语言支持 在构造时将错误信息存为 std::locale/std::wstring,或使用 ICU / gettext 进行本地化。
重试机制 对于可恢复错误,可在异常中存储 retryCount_suggestedDelay_
堆栈追踪 C++20 std::stacktrace(或第三方库)可在构造时捕获,并在 what() 中打印。
不可抛异常 对于不可恢复错误,建议直接返回错误码或使用 std::optional / std::expected(C++23)。

6. 典型错误处理模式

// 1. 业务函数
bool parseJson(const std::string& data, Json& out) {
    try {
        out = Json::parse(data);
    }
    catch (const Json::ParseError& e) {
        throw JsonParseException(data, e.what());
    }
    return true;
}

// 2. 入口层
int main() {
    try {
        std::string raw = readFile("config.json");
        Json config;
        parseJson(raw, config);
        // ...
    }
    catch (const JsonParseException& e) {
        std::cerr << "JSON error: " << e.what() << '\n';
        return EXIT_FAILURE;
    }
    catch (const std::exception& e) {
        std::cerr << "Unhandled exception: " << e.what() << '\n';
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

此模式将错误向上传递,最终在 main 中统一捕获并记录日志,保持业务代码简洁。


7. 小结

  • 自定义异常 让错误处理更语义化,方便定位与维护。
  • 继承 std::exceptionstd::runtime_error,保持与标准库的一致性。
  • 携带丰富信息(错误码、上下文、建议)可显著提升调试效率。
  • 异常链(nested exceptions) 可以用 std::throw_with_nested 在更高层捕获时保留底层错误。

通过上述思路,你可以在任何 C++ 项目中构建完善、易维护的异常体系,提升代码质量与可维护性。祝编码愉快!

发表评论