继承 std::exception 时必须用类内 std::string 成员保存错误消息并重写 const noexcept 的 what() 方法,否则 what() 返回悬垂指针导致未定义行为;还需显式声明 virtual 析构函数,并以 const& 方式捕获异常。

为什么继承 std::exception 不能只写空构造函数
直接继承 std::exception 并只定义一个空构造函数,会导致 what() 返回的字符串不可控甚至崩溃。因为 std::exception 的默认实现不保存任何消息,其 what() 返回的是未定义行为(常见为指向已销毁栈内存的指针),尤其在抛出后被多次调用时极易出错。
- 必须确保
what()返回的 C 字符串生命周期长于异常对象本身存活期 - 不能返回局部
std::string::c_str()或临时std::string的内部指针 - 推荐在类内持有一个
std::string成员,用它来提供稳定地址
如何安全重写 what() 方法(含 const 和 noexcept)
what() 必须声明为 const noexcept,否则无法通过 std::exception 接口被正确调用;同时返回类型必须是 const char*,且指向的内容不能随对象析构而失效。
class FileOpenError : public std::exception {
private:
std::string message_;
public:
explicit FileOpenError(const std::string& file)
: message_("Failed to open file: " + file) {}
const char* what() const noexcept override {
return message_.c_str();
}};
-
message_是类内std::string成员,保证字符串存储在堆上、生命周期与异常对象一致 -
noexcept是强制要求:C++ 标准规定std::exception::what()是noexcept,子类重写也必须保持相同异常规范 - 不要在
what()里拼接新字符串或调用可能抛异常的函数
要不要加 virtual 析构函数?
要。虽然 std::exception 的析构函数已是 virtual,但显式写出能避免误删派生类资源(比如你后续在自定义异常中添加了动态分配的缓冲区或文件句柄)。
立即学习“C++免费学习笔记(深入)”;
- 即使当前没资源要清理,也建议统一加上
virtual ~FileOpenError() = default; - 不加不会立即报错,但若未来扩展异常类携带资源,就容易发生析构不完整、内存泄漏等问题
- 所有多态基类都应该有
virtual析构函数——这是 C++ 基本守则,不是可选项
实际使用时容易忽略的细节
捕获时别用值传递,也别漏掉 const&;日志打印前先确认 what() 是否真的可用;跨 DLL 边界抛异常需格外小心。
- 错误写法:
catch (FileOpenError e)—— 触发不必要的拷贝,还可能切片(如果从更深层派生) - 正确写法:
catch (const FileOpenError& e)或catch (const std::exception& e) - 在
catch块里直接用e.what()是安全的,但不要把它存成裸指针长期持有 - Windows 下若异常跨越 DLL 边界(如 DLL 抛出、EXE 捕获),
std::exception及其派生类可能因 ABI 不兼容而崩溃;此时应改用错误码或跨 ABI 的结构体传信息
标准库异常体系不鼓励深度继承,但只要守住 what() 的生命周期、noexcept 约束和虚析构这三点,自定义异常就能稳住。最常翻车的地方,其实是把 what() 写成返回临时字符串的指针——这个坑,几乎每个初学者都踩过一次。










