先抛出一个结论:能用 try-catch 捕获的,只有真正被 throw 抛出的 C++ 异常。 像空指针解引用、除零这类系统级错误,默认是不会触发 catch 的,除非编译器开启特定异常机制(比如 Windows 的 SEH 转换)。
看这张图,直观展示了 C++ 异常处理的边界:
什么时候 catch 会完全失效?
很多人以为 catch(...) 能抓一切,真相远没那么简单。它只捕获 C++ 异常对象,对下面这些情况完全无感:
- 访问野指针或
nullptr导致的段错误(SIGSEGV),在 Linux/macOS 下直接进程终止,catch 根本来不及反应 int x = 1 / 0;—— 这是 CPU 级别的 trap,C++ 标准压根不把它定义为异常- 栈溢出、内存耗尽(
new失败时,只有开了nothrow才会抛std::bad_alloc)
验证方法很简单:写个 throw 42; 能被捕获;但 *(int*)0 = 1; 会直接 crash,catch(...) 连运行的机会都没有。
throw 表达式必须匹配 catch 类型吗?
必须匹配,但规则比表面严格。不是“能转就行”,而是要求类型相同、公有继承、或有可调用的拷贝/移动构造函数:
throw std::runtime_error("msg");→ 可被catch(std::exception& e)捕获(公有继承)throw "hello";→ 只能被catch(const char*)捕获,catch(std::string)不行(隐式转换不参与匹配)throw 3.14;→catch(int)捕不到,catch(double)才行
核心建议:始终抛 std::exception 派生类,避免基础类型传递导致的切片或生命周期问题。
为什么 catch 参数推荐用引用?
值传递会触发异常对象拷贝,可能抛新异常(比如拷贝构造函数里 new 失败),导致栈展开中断、程序终止。看这个例子:
class BadCopy {public: BadCopy() = default; BadCopy(const BadCopy&) { throw std::runtime_error("copy failed"); }};// ...try { throw BadCopy{};} catch(BadCopy b) { /* 这里会二次崩溃 */ }正确写法只有一种:
catch(const std::exception& e)—— 安全、高效、支持多态- 避免
catch(std::exception e)(值传递)和catch(std::exception&& e)(仅匹配右值,漏掉大部分情况)
资源泄漏风险:throw 发生在构造函数里怎么办?
构造函数中途 throw,已构造的成员会自动析构,但裸指针、文件描述符等不会。看个错例:
- 错例:
FILE* fp = fopen("x.txt", "r"); if (!fp) throw std::runtime_error("open fail");——fopen成功后若后续 throw,fp泄漏 - 正解:用 RAII 封装,比如
std::unique_ptr或std::ifstream - 更彻底的做法:把资源获取逻辑移到工厂函数中,构造函数只做轻量初始化
记住:C++ 异常安全的唯一可靠路径是 RAII,不是靠 try-catch 到处兜底。
话题回到最容易忽略的一点:noexcept 函数里如果意外抛出异常,程序会立刻调用 std::terminate,连 catch(...) 都来不及触发。检查第三方库接口是否标记 noexcept,比写 try 更关键。