异常是程序在无法按正常路径继续时,向调用链上层报告错误的一种机制。抛出异常后,控制沿栈展开直至匹配的 catch 处理;未捕获异常通常导致 std::terminate。本节说明 try、throw、catch 的基本语法、标准异常层次、与错误码的选择,以及与 RAII 的关系。
基本结构
#include <iostream>
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0) {
throw std::invalid_argument("divisor is zero");
}
return a / b;
}
int main() {
try {
std::cout << divide(10, 0) << std::endl;
} catch (const std::invalid_argument& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}catch 按类型匹配;派生类处理器应写在基类之前,否则派生对象可能被基类 catch 截获。捕获常用 const std::exception& 只读访问 what();捕获非引用会切片,应避免 catch (std::exception e)。
标准异常层次(节选)
| 类型 | 典型含义 |
|---|---|
std::logic_error 派生 | 编程错误,如 invalid_argument、out_of_range |
std::runtime_error 派生 | 运行期错误,如 overflow_error |
std::bad_alloc | new 失败 |
库函数在参数无效时常抛 std::out_of_range(如 vector::at)。自定义类型可继承 std::exception 并实现 what()。
栈展开与 RAII
异常从 work 抛到 main 的 catch 时,中间尚未离开的局部对象会按声明的反序析构,这就叫栈展开。因此 FileGuard、unique_ptr、lock_guard、文件流等 RAII 对象仍会释放资源。若只用裸 new 且未在抛出前 delete,这块内存就会泄漏。下面与析构函数里的 FileGuard 相同:即使 throw 了,也会先打印 close 再进入 catch。
#include <iostream>
#include <stdexcept>
class FileGuard {
public:
FileGuard() { std::cout << "open\n"; }
~FileGuard() { std::cout << "close\n"; }
};
void work() {
FileGuard g; // 先 open
throw std::runtime_error("fail"); // 离开 work 前仍析构 g
}
int main() {
try {
work();
} catch (const std::runtime_error& e) {
std::cout << e.what() << "\n";
}
return 0;
}输出:
open
close
fail若把 FileGuard g 换成 int* p = new int(1) 且不 delete,throw 之后 p 指向的内存不会自动释放。日常应使用智能指针或上述包装类。
重新抛出与嵌套
内层可以先记下日志,再用无操作数的 throw; 把同一个异常原样交给外层。这与 throw e; 不同:后者可能切片或换成副本。下面 middle 嵌套在 main 的 try 里,inner 抛出后先被 middle 接到,再重新抛到 main。
#include <iostream>
#include <stdexcept>
void inner() {
throw std::runtime_error("disk full");
}
void middle() {
try {
inner();
} catch (...) {
std::cout << "middle: log and rethrow\n";
throw; // 原样抛给上层,类型仍是 runtime_error
}
}
int main() {
try {
middle();
} catch (const std::runtime_error& e) {
std::cout << "main: " << e.what() << "\n";
}
return 0;
}输出:
middle: log and rethrow
main: disk fullcatch (...) 能接到任意类型,但读不到 what(),所以这里只记一句再 throw;。若内层已经处理完毕、不必再往上走,就不要重新抛出。
何时使用异常
- 适合:无法本地恢复的致命错误、构造函数失败(无法返回错误码)、跨多层传播的错误。
- 慎用:预期内的控制流、高频路径;可考虑
std::optional或错误码(optional)。 noexcept:承诺不抛异常的函数可标记noexcept,违反时直接terminate;移动操作常标记noexcept以利容器优化。
与 C 语言返回错误码不同,异常会跳过中间层的显式检查,但代价是控制流不直观;团队应统一边界风格(库接口抛异常还是返回 expected / optional)。
本节说明了异常处理的基本流程。下一节介绍文件流:用 fstream 读写磁盘文件。