在现代 C++ 开发中,异常处理是保证程序健壮性的关键手段。虽然标准库提供了许多现成的异常类型(如 std::runtime_error、std::logic_error 等),但在实际项目中往往需要根据业务需求创建自己的异常类。本文将从设计原则、继承体系、构造方式以及多态捕获等方面,系统阐述如何在 C++ 中实现并使用自定义异常类。
1. 设计原则
-
异常类型要与错误语义相匹配
每一种异常都应对应一种可辨识的错误状态,方便调用者在catch块中精确处理。 -
遵循异常安全的 RAII
异常对象应满足 对象生命周期 的规则,即在抛出时应已经完全构造好,且在被捕获后可以安全析构。 -
不要抛弃信息
异常对象应携带足够的错误信息(如错误码、上下文描述等),以便调试和日志记录。 -
异常继承自
std::exception或std::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::exception或std::runtime_error,保持与标准库的一致性。 - 携带丰富信息(错误码、上下文、建议)可显著提升调试效率。
- 异常链(nested exceptions) 可以用
std::throw_with_nested在更高层捕获时保留底层错误。
通过上述思路,你可以在任何 C++ 项目中构建完善、易维护的异常体系,提升代码质量与可维护性。祝编码愉快!