最稳妥的异常设计是继承 std::runtime_error;需用 explicit 防隐式转换,避免资源管理与拷贝,抛出时就地构造,捕获用 const& 防切片。

继承 std::exception 是最稳妥的起点
直接用 throw 抛出字符串或裸指针,虽然能编译,但会丢失类型信息、无法被 catch (const std::exception&) 统一捕获,且析构不安全。标准做法是让自定义异常类继承 std::exception 或其子类(如 std::runtime_error)。
推荐从 std::runtime_error 派生——它已实现 what()、持有字符串、满足异常对象应为临时/轻量的要求:
class FileOpenError : public std::runtime_error {
public:
explicit FileOpenError(const std::string& path)
: std::runtime_error("Failed to open file: " + path), path_(path) {}
const std::string& path() const { return path_; }
private:
std::string path_;
};
- 必须用
explicit防止隐式转换构造 - 不要在异常类里放大对象或动态资源(如
std::vector大缓冲区),避免抛出时触发额外异常 - 不要重写
what()除非需要运行时拼接;继承std::runtime_error已自动支持
throw 后的对象生命周期由编译器管理,别返回局部对象引用
常见错误是这么写:
FileOpenError make_error() {
FileOpenError e("config.txt");
return e; // ❌ 危险:可能触发拷贝,且若禁用 RVO 或异常处理路径复杂,易出问题
}
正确做法是直接 throw,让编译器在栈展开时构造异常对象:
立即学习“C++免费学习笔记(深入)”;
void load_config(const std::string& path) {
if (!std::filesystem::exists(path)) {
throw FileOpenError(path); // ✅ 对象在 throw 点就地构造,生命周期明确
}
}
- 所有异常对象都应是可复制或可移动的(
std::exception体系默认满足) - 不要在
catch块里return异常对象本身,而应重新throw(throw;)以保持原始类型和堆栈语义 - 如果需要附加上下文,应在抛出前组装好,而不是靠
catch后再“增强”异常对象
捕获时优先用 const&,避免切片和无谓拷贝
写成 catch (FileOpenError e) 会触发一次拷贝,还可能因多态截断(如果实际抛出的是派生类)。更糟的是,若异常对象不可拷贝,代码直接编译失败。
统一用 const& 捕获:
try {
load_config("/etc/app.conf");
} catch (const FileOpenError& e) {
std::cerr << "Path: " << e.path() << ", Msg: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cerr << "Other error: " << e.what() << "\n";
}
- 即使你只读
what(),也建议用const std::exception&做兜底,而非...(省略号),否则会漏掉非std::exception派生的异常(比如throw 42;) - 不要在
catch块里修改异常对象状态(它只是只读视图) - 多个
catch分支注意顺序:子类必须写在父类之前,否则会被提前捕获
跨 DLL / SO 边界抛异常极危险,生产环境尽量避免
Windows 上不同模块(EXE/DLL)用不同 CRT 实例时,throw 一个在 DLL 中构造的异常对象,到 EXE 中 catch,可能导致 what() 返回乱码、析构崩溃,甚至程序终止。Linux 的 SO 类似,尤其涉及 STL 类型时 ABI 不保证兼容。
真实项目中更可靠的做法是:
- 在接口层(如 C API 或纯虚函数)统一用错误码(
int或enum)传递失败原因 - 内部模块仍可用自定义异常,但绝不让它跨二进制边界
- 若必须跨边界,只抛标准库异常(如
std::runtime_error),并确保所有模块用同一版本 STL 和编译器设置(如_GLIBCXX_USE_CXX11_ABI)
这点容易被忽略——本地测试全绿,一上生产环境换了个构建配置或依赖库版本,就崩在 catch 里。









