C++异常处理全解析:从标准库异常到内存管理避坑指南 1. 项目概述为什么我们需要一份C异常“避坑指南”干了这么多年C开发我敢说异常处理这块儿绝对是新手和老手都容易栽跟头的地方。你可能觉得不就是try、catch、throw三个关键字嘛有啥难的但真到了项目里面对一个突然崩溃的程序或者一个难以复现的诡异bug你盯着日志里那句“terminate called after throwing an instance of std::logic_error”是不是经常一头雾水不知道从何查起这就是为什么我想系统地梳理一份C常见异常汇总。这不仅仅是罗列错误类型更是想把我这些年调试时踩过的坑、总结的经验以及如何从异常信息里快速定位问题根源的方法一股脑儿分享给你。无论你是正在学习C语法还是已经在一线写业务代码一份清晰的异常“地图”都能让你在遇到问题时不再像个无头苍蝇而是能冷静分析快速解决。今天这篇“汇总一”我们先从最基础、最核心的标准库异常和内存相关异常讲起这些都是你几乎每天都会打交道的“老朋友”和“捣蛋鬼”。2. C异常处理机制快速回顾在深入具体异常之前我们得先统一一下“语言”。C的异常处理机制其核心思想是“抛出”和“捕获”。当一段代码执行过程中遇到了无法或不应在本地处理的错误时它可以选择“抛出”一个异常对象。这个异常对象会沿着函数调用栈向上“冒泡”直到被某个catch块“捕获”并处理。如果一直没被捕获程序就会调用std::terminate通常导致程序崩溃。2.1 基本语法与执行流程我们用一段最简单的代码来感受一下这个流程#include iostream #include stdexcept double safe_divide(int numerator, int denominator) { if (denominator 0) { // 1. 抛出异常创建一个 std::runtime_error 对象并抛出 throw std::runtime_error(Division by zero!); } return static_castdouble(numerator) / denominator; } int main() { int a 10, b 0; try { // 2. try块包裹可能抛出异常的代码 std::cout Result: safe_divide(a, b) std::endl; } catch (const std::runtime_error e) { // 3. catch块捕获特定类型的异常并进行处理 std::cerr Caught an error: e.what() std::endl; // 可能的处理返回错误码、记录日志、使用默认值等 // return EXIT_FAILURE; } catch (...) { // 4. 捕获所有未被前面catch处理的异常 std::cerr Caught an unknown exception! std::endl; } // 5. 如果异常被成功捕获并处理程序会继续执行这里的代码 std::cout Program continues normally. std::endl; return 0; }这段代码的执行路径非常清晰main调用safe_divide-b为0触发throw- 异常被抛出safe_divide函数立即终止返回值无效 - 控制权回到main的try块但try块内throw之后的代码std::cout不会执行 - 系统在调用栈中寻找匹配的catch处理器找到了catch (const std::runtime_error e)- 执行该catch块内的代码打印错误信息 -catch块执行完毕程序跳转到所有catch块之后继续执行。注意catch的参数最好使用const引用如const std::exception。这避免了不必要的异常对象拷贝如果异常类型支持移动构造有时也使用值捕获但引用是通用且高效的选择更重要的是它能正确捕获所有派生类异常。比如用const std::exception可以捕获std::runtime_error、std::logic_error等。2.2 异常安全基础一提到异常就绕不开“异常安全”这个概念。它指的是当异常被抛出时程序的状态不会因此被破坏如资源泄漏、数据不一致。C标准库对异常安全有基本保证但我们的自定义代码需要格外小心。最简单的一个原则是RAII。假设我们有一个简单的类它在构造函数中申请资源在析构函数中释放class FileHandler { public: FileHandler(const std::string filename) : m_file(nullptr) { m_file fopen(filename.c_str(), r); // 可能失败 if (!m_file) { // 构造函数内抛出异常是安全的因为对象尚未完全构造完成 // 析构函数不会被调用所以我们需要自己清理已分配的资源 // 更好的做法是使用标准库如fstream这里仅为演示 throw std::runtime_error(Failed to open file: filename); } // ... 其他初始化 } ~FileHandler() { if (m_file) { fclose(m_file); } } // 禁用拷贝构造和赋值避免重复释放或实现深拷贝 FileHandler(const FileHandler) delete; FileHandler operator(const FileHandler) delete; private: FILE* m_file; };在这个例子中如果fopen失败我们在构造函数中抛出异常。由于对象还没有成功构造它的析构函数不会被调用因此我们必须在抛出前确保没有资源泄漏这里m_file是nullptr所以没问题。这是一种基本的异常安全保证。更健壮的做法是使用智能指针如std::unique_ptr配合自定义删除器来管理资源这样即使初始化过程中发生异常智能指针的析构也会确保资源被释放。3. 标准库异常家族详解C标准库定义了一个异常类的层次结构所有标准异常都派生自std::exception基类。理解这个家族图谱是快速识别异常类型的关键。我们先看一个简化版的继承关系std::exception ├── std::bad_alloc ├── std::bad_cast ├── std::bad_typeid ├── std::bad_exception ├── std::logic_error │ ├── std::domain_error │ ├── std::invalid_argument │ ├── std::length_error │ └── std::out_of_range └── std::runtime_error ├── std::overflow_error ├── std::underflow_error ├── std::range_error └── std::system_error (C11起)3.1 逻辑错误std::logic_error 及其子类这类异常通常表示程序逻辑上的错误是“程序员该背的锅”。理论上通过代码审查和测试这些错误是可以在运行前被发现的。std::logic_error所有逻辑错误的基类。通常表示程序内部逻辑不一致。std::invalid_argument当传递给函数的参数值不被接受时抛出。这是你最容易遇到也最应该抛出的异常之一。void set_age(int age) { if (age 0 || age 150) { throw std::invalid_argument(Age must be between 0 and 150.); } // ... 设置年龄 }std::out_of_range当访问容器如std::vector、std::string、std::array时索引或位置超出有效范围时抛出。std::vectorint vec {1, 2, 3}; try { int val vec.at(10); // 使用 .at() 会进行边界检查并可能抛出 std::out_of_range // int val vec[10]; // 使用 operator[] 通常不检查行为未定义可能崩溃或读取垃圾值 } catch (const std::out_of_range e) { std::cerr Out of range error: e.what() std::endl; }实操心得在调试阶段或对安全性要求高的代码中尽量使用容器的.at()成员函数来访问元素因为它会进行边界检查并抛出清晰的异常。而在性能关键的、已经确保索引安全的循环内部可以使用operator[]来避免检查开销。std::length_error当操作试图创建一个超出该对象最大允许长度的对象时抛出典型场景是创建过长的std::string虽然现在很少见因为现代实现通常只受内存限制或对某些容器进行超出max_size()的操作。std::domain_error用于数学函数当参数不在函数定义的域内时抛出。例如对负数取平方根虽然标准数学库通常返回NaN而不是抛异常但你可以自定义函数使用它。3.2 运行时错误std::runtime_error 及其子类这类错误通常与程序逻辑无关而是由外部因素或无法在编码时预测的条件引起的比如资源不足、硬件故障或无效的用户输入从广义上讲输入验证不严也是逻辑错误但通常放在运行时处理。std::runtime_error所有运行时错误的基类。用途非常广泛。std::overflow_error/std::underflow_error分别表示算术运算上溢结果超出类型能表示的最大值和下溢结果非零但太小无法以正常精度表示。注意C内置类型的算术运算如int相加溢出通常不会抛出这些异常而是产生未定义行为或环绕。这些异常主要用于高精度数学库或自定义数值类型。std::range_error当存储一个值而该值超出了对象允许的范围时抛出。例如在某些转换或舍入操作中。std::system_error(C11)这是非常重要的一个子类它封装了操作系统错误码errno和错误信息。当你进行系统调用如文件操作、网络通信、线程操作失败时应该优先考虑抛出或使用std::system_error。#include system_error #include fstream void open_file(const std::string path) { std::ifstream file(path); if (!file.is_open()) { // 使用 std::io_errc::stream 错误类别和当前的 errno 构造 system_error throw std::system_error(errno, std::generic_category(), Failed to open file: path); } }3.3 其他核心标准异常std::bad_alloc当new运算符或某些容器如std::vector在扩容时无法分配请求的内存时抛出。这是内存管理中最关键的异常。std::bad_cast当dynamic_cast对引用类型进行向下转型失败时抛出对指针类型失败则返回nullptr。class Base { public: virtual ~Base() {} }; class Derived : public Base {}; Base* basePtr new Base; try { Derived derivedRef dynamic_castDerived(*basePtr); // 错误抛出 std::bad_cast } catch (const std::bad_cast e) { std::cerr Bad cast: e.what() std::endl; } delete basePtr;std::bad_typeid当typeid运算符应用于一个解引用值为空nullptr的指针时抛出。Base* ptr nullptr; try { std::cout typeid(*ptr).name() std::endl; // 错误抛出 std::bad_typeid } catch (const std::bad_typeid e) { std::cerr Bad typeid: e.what() std::endl; }4. 内存相关异常深度解析内存问题是C程序崩溃的“头号杀手”而std::bad_alloc是其最直接的代言人。但它的背后远不止一个简单的“内存不足”。4.1 std::bad_alloc 的触发场景与应对当new表达式无法满足内存分配请求时它会抛出std::bad_alloc。但“无法满足”可能源于多种情况物理内存耗尽这是最直接的原因但在现代操作系统中由于虚拟内存和过度提交overcommit策略单纯申请大块内存更容易触发此异常而零碎的内存分配可能导致OOM Killer先一步干掉进程。地址空间碎片化即使总空闲内存足够但可能没有一块连续的空闲地址空间能满足当前分配请求的大小。这在长时间运行、频繁进行不同大小内存分配/释放的32位程序中更常见。内存分配器限制某些环境或自定义分配器可能设置了单个分配块的上限。应对策略不仅仅是try-catch#include new // 对于 std::bad_alloc #include iostream #include vector #include memory void handle_large_allocation() { const size_t huge_size 1024ULL * 1024 * 1024 * 10; // 10 GB int* big_array nullptr; try { big_array new int[huge_size]; // 可能抛出 std::bad_alloc // ... 使用数组 delete[] big_array; } catch (const std::bad_alloc e) { std::cerr Memory allocation failed: e.what() std::endl; // 1. 降级策略尝试分配更小的内存 // 2. 清理缓存释放一些非关键内存 // 3. 优雅降级关闭部分功能保存用户数据 // 4. 记录日志并通知用户 std::cerr Switching to fallback mode or exiting gracefully. std::endl; // 注意确保这里没有内存泄漏big_array 在异常抛出时是 nullptr无需 delete } }更现代的做法是使用**std::nothrow**版本它不会抛出异常而是在分配失败时返回nullptrint* ptr new(std::nothrow) int[1000000000LL]; if (ptr nullptr) { // 处理分配失败 std::cerr Allocation failed (nothrow). std::endl; }但请注意std::vector、std::string等容器在扩容时如果分配失败依然会抛出std::bad_alloc。你不能用nothrow来构造它们。对于容器一种策略是使用reserve()预分配足够内存如果预分配失败在程序早期就能发现问题。4.2 自定义 new_handlerC允许你设置一个全局的new_handler函数。当new无法分配内存时在抛出std::bad_alloc之前它会反复调用这个处理函数。你可以在处理函数里尝试释放一些内存比如清空全局缓存然后返回让new再次尝试分配。如果处理函数无法释放内存它应该抛出std::bad_alloc、调用std::abort()或直接退出程序。#include iostream #include new #include cstdlib void my_new_handler() { std::cerr My new handler called: Attempting to free some memory... std::endl; // 这里可以尝试释放一些预先分配的应急内存、清理缓存等。 // 如果还是无法满足需求我们选择终止程序。 std::cerr Insufficient memory. Terminating. std::endl; std::abort(); // 或 throw std::bad_alloc(); } int main() { std::set_new_handler(my_new_handler); // 现在任何内存分配失败都会先调用 my_new_handler try { while (true) { char* p new char[100000000]; // 持续分配最终会触发 } } catch (const std::bad_alloc) { // 如果 my_new_handler 抛出了 bad_alloc这里会捕获到 std::cerr Caught bad_alloc in main. std::endl; } return 0; }4.3 内存泄漏与异常安全异常安全与内存泄漏紧密相关。考虑下面这个有问题的函数void risky_function() { int* resource1 new int(100); some_operation_that_might_throw(); // 可能抛出异常 int* resource2 new int(200); // ... 使用 resource1 和 resource2 delete resource1; delete resource2; }如果some_operation_that_might_throw()抛出了异常那么resource1指向的内存就永远无法被释放导致内存泄漏。这就是典型的异常不安全代码。解决方案1使用智能指针首选#include memory void safe_function() { auto resource1 std::make_uniqueint(100); // C14 // auto resource1 std::unique_ptrint(new int(100)); // C11 some_operation_that_might_throw(); auto resource2 std::make_uniqueint(200); // ... 使用 *resource1 和 *resource2 // 无需手动 deleteunique_ptr 超出作用域时会自动释放内存。 // 即使中间发生异常栈展开过程也会保证已构造的 unique_ptr 被正确析构。 }std::unique_ptr和std::shared_ptr是管理动态内存的利器它们利用RAII确保在异常发生时资源能被自动释放。解决方案2使用标准库容器大多数情况下你根本不需要手动new/delete。std::vector、std::string等容器自己管理内存并且是异常安全的。解决方案3编写异常安全的裸指针代码不推荐仅作理解如果必须使用裸指针确保在异常发生前清理资源。这通常需要将代码重构成强异常安全保证的形式或者使用“资源获取即初始化”的包装类。5. 容器与算法相关异常实战标准库容器和算法是C的基石它们内部也大量使用了异常来报告错误。理解这些异常触发的具体条件能帮你写出更健壮的代码。5.1 std::vector 与 std::string 的边界问题我们之前提到了.at()会抛出std::out_of_range。但operator[]的行为呢对于std::vector和std::string使用operator[]访问越界索引是未定义行为。它可能崩溃可能返回垃圾值也可能看似正常工作但埋下隐患。绝对不要依赖它。std::vectorint v {1, 2, 3}; // 错误未定义行为 // int x v[5]; // v[5] 10; // 正确做法使用 at() 进行边界检查 try { int x v.at(5); // 抛出 std::out_of_range } catch (const std::out_of_range e) { // 处理越界 } // 或者在访问前手动检查 size_t index 5; if (index v.size()) { int x v[index]; } else { // 处理错误 }对于std::string的substr操作如果起始位置pos大于字符串长度也会抛出std::out_of_range。5.2 迭代器失效与异常迭代器失效本身通常不会直接抛出异常但它会导致后续使用失效迭代器时产生未定义行为这比抛出异常更危险。常见的失效场景包括向std::vector、std::string插入/删除元素可能导致所有迭代器失效。从std::deque中间插入/删除可能导致所有迭代器失效。从std::map、std::set、std::unordered_map、std::unordered_set中删除元素只会使指向被删除元素的迭代器失效。一个典型的坑在遍历容器时删除元素。std::vectorint vec {1, 2, 3, 4, 5}; for (auto it vec.begin(); it ! vec.end(); it) { if (*it % 2 0) { vec.erase(it); // 错误erase 后 it 失效后续的 it 行为未定义 } }正确做法是使用erase返回的新的有效迭代器for (auto it vec.begin(); it ! vec.end(); /* 不在这里递增 */) { if (*it % 2 0) { it vec.erase(it); // erase 返回被删除元素之后元素的迭代器 } else { it; } }或者对于顺序容器C11后可以使用“擦除-移除”惯用法vec.erase(std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 0; }), vec.end());5.3 算法中的异常标准库算法如std::sort,std::copy,std::transform本身通常不直接抛出异常除非内存分配失败内部调用new。但是它们可能会调用用户提供的函数对象如比较函数、谓词、转换函数如果这些用户函数抛出异常算法的异常安全性就至关重要。大多数标准库算法提供基本异常保证如果用户函数抛出异常算法会保证容器处于有效状态所有元素都是可析构的但容器的具体内容顺序、值是未指定的。这意味着程序不会崩溃但数据可能处于一个中间状态。例如std::sort使用比较函数。如果比较函数抛出异常std::sort会捕获它并保证序列仍然是有效的所有元素都存在但顺序可能是部分排序的不再是原始顺序也不是完全排序的顺序。你需要自己决定是否要重试排序或进行其他恢复操作。std::vectorint data {5, 3, 1, 4, 2}; try { std::sort(data.begin(), data.end(), [](int a, int b) { if (a 3 b 1) { throw std::runtime_error(Comparison failed!); } return a b; }); } catch (const std::runtime_error e) { std::cerr Sorting interrupted: e.what() std::endl; // 此时 data 的内容是有效的但顺序未知。 // 例如可能是: {1, 3, 5, 4, 2} 或其他部分排序状态。 for (int x : data) std::cout x ; std::cout std::endl; }6. 类型转换与动态类型识别异常C的类型系统相对静态但运行时类型信息RTTI和动态转换提供了在运行时处理多态类型的能力与之相关的异常是std::bad_cast和std::bad_typeid。6.1 dynamic_cast 与 std::bad_castdynamic_cast用于在继承层次结构中安全地进行向下或交叉转换。它对指针和引用的行为不同对指针如果转换失败返回nullptr。对引用如果转换失败抛出std::bad_cast异常。为什么有这个区别因为引用在C中不能为null没有一个“空引用”的状态来表示失败所以只能用异常来报告错误。class Animal { public: virtual ~Animal() {} }; class Dog : public Animal { public: void bark() { std::cout Woof!\n; } }; class Cat : public Animal {}; void process_animal_ptr(Animal* animal) { Dog* dog dynamic_castDog*(animal); if (dog) { dog-bark(); // 安全调用 } else { std::cout Not a dog pointer.\n; } } void process_animal_ref(Animal animal) { try { Dog dog dynamic_castDog(animal); // 如果 animal 不是 Dog抛出 std::bad_cast dog.bark(); } catch (const std::bad_cast e) { std::cout Caught bad_cast: e.what() . Not a dog reference.\n; } } int main() { Dog dog; Cat cat; process_animal_ptr(dog); // 输出: Woof! process_animal_ptr(cat); // 输出: Not a dog pointer. process_animal_ref(dog); // 输出: Woof! process_animal_ref(cat); // 输出: Caught bad_cast: std::bad_cast. Not a dog reference. return 0; }使用建议如果你不确定转换是否成功并且有合理的失败处理路径比如尝试另一种转换优先使用指针版本的dynamic_cast并检查nullptr。如果你非常确定转换应该成功或者转换失败是一个严重的、不可恢复的程序逻辑错误那么可以使用引用版本让异常来暴露这个错误。6.2 typeid 与 std::bad_typeidtypeid运算符用于获取对象的类型信息返回一个std::type_info对象的引用。当typeid的操作数是一个多态类型有虚函数的表达式时它会进行运行时计算返回表达式所指向的实际动态类型的type_info。如果对空指针解引用进行typeid操作就会抛出std::bad_typeid。Base* ptr1 new Derived; Base* ptr2 nullptr; // 正确typeid(*ptr1) 会调用 RTTI得到 Derived 的类型信息 std::cout typeid(*ptr1).name() std::endl; try { // 错误对空指针解引用抛出 std::bad_typeid std::cout typeid(*ptr2).name() std::endl; } catch (const std::bad_typeid e) { std::cerr bad_typeid caught: e.what() std::endl; } delete ptr1;注意事项typeid在用于非多态类型没有虚函数的类或非类类型时是在编译时确定的不会引发运行时开销也不会抛出std::bad_typeid因为操作数不是指针/引用解引用。另外typeid返回的std::type_info对象的.name()方法返回的实现定义的名称可能是一个混淆过的名字如果需要可读的名称可以考虑使用编译器特定的函数如GCC/Clang的abi::__cxa_demangle或第三方库。7. 自定义异常的设计与最佳实践虽然标准库异常覆盖了很多场景但在实际项目中定义自己的异常类能让错误信息更精确处理更灵活。7.1 如何继承 std::exception自定义异常类应该公有继承自std::exception或其标准子类如std::runtime_error。继承std::runtime_error或std::logic_error是最方便的做法因为它们已经处理了错误信息的存储。#include stdexcept #include string class MyFileException : public std::runtime_error { public: explicit MyFileException(const std::string filename, const std::string operation) : std::runtime_error(File error: operation failed for filename ), m_filename(filename), m_operation(operation) {} const std::string get_filename() const { return m_filename; } const std::string get_operation() const { return m_operation; } private: std::string m_filename; std::string m_operation; }; // 使用 void read_config_file(const std::string path) { std::ifstream file(path); if (!file) { throw MyFileException(path, open); } // ... 读取文件如果失败 if (/* read error */) { throw MyFileException(path, read); } }7.2 异常规格说明 (Exception Specifications) 与 noexcept在C11之前可以使用动态异常规格如void func() throw(std::bad_alloc, MyException);来声明函数可能抛出的异常类型。但这种机制在C11中被弃用并在C17中移除因为它带来的问题运行时检查开销、维护负担多于好处。C11引入了noexcept说明符它更简单、高效。void func() noexcept;表示func承诺不会抛出任何异常。如果它抛出了异常程序会直接调用std::terminate()终止。这允许编译器进行更多优化。void func() noexcept(true/false);是条件形式。移动构造函数和移动赋值运算符通常应该标记为noexcept如果它们确实不抛异常这能让标准库容器如std::vector在重新分配内存时更高效地使用移动而非拷贝。class MyMovableType { public: MyMovableType(MyMovableType other) noexcept { /* 移动资源 */ } MyMovableType operator(MyMovableType other) noexcept { if (this ! other) { // 移动资源 } return *this; } // ... 其他成员 };最佳实践总结优先使用标准异常除非有非常特定的需求否则尽量使用std::runtime_error,std::invalid_argument等。它们通用容易被其他开发者理解。按错误类别继承如果确实需要自定义根据错误性质继承std::logic_error程序逻辑错误或std::runtime_error运行时外部错误。提供丰富的上下文信息在异常消息中包含尽可能多的调试信息如文件名、行号可用宏__LINE__、函数名、相关变量值等。但注意不要包含敏感信息。小心异常的内存管理确保自定义异常类本身的构造和拷贝不会抛出异常特别是what()返回的字符串否则可能导致std::terminate被调用。使用noexcept对于明确不会抛出异常的函数如简单的getter、移动操作使用noexcept进行修饰。避免在析构函数中抛出异常如果析构函数在栈展开过程中因为另一个异常而被调用此时再抛出异常会导致程序立即终止std::terminate。确保析构函数是noexcept的。8. 调试技巧与常见问题排查当程序抛出异常时光看异常类型和what()信息有时还不够。我们需要知道异常是在哪里抛出的调用栈是怎样的。8.1 获取异常抛出点的调用栈在Linux/macOS下你可以使用backtrace系列函数。在Windows下可以使用CaptureStackBackTrace。但更跨平台、更方便的做法是使用第三方库如boost::stacktraceC库或集成像gdb/lldb这样的调试器。一个简单的示例在捕获异常时打印简易栈信息需要编译时加上-rdynamic等选项以支持函数名解析#include iostream #include stdexcept #include execinfo.h // Linux/macOS 特定 #include cstdlib #include cxxabi.h // 用于名字反修饰 void print_stacktrace() { const int max_frames 100; void* buffer[max_frames]; int num_frames backtrace(buffer, max_frames); char** symbols backtrace_symbols(buffer, num_frames); if (symbols) { std::cerr Stack trace ( num_frames frames):\n; for (int i 0; i num_frames; i) { // 可以尝试解析 symbols[i] 以得到更可读的函数名 std::cerr symbols[i] std::endl; } free(symbols); } } void deep_function() { throw std::runtime_error(Error from deep within!); } void intermediate_function() { deep_function(); } int main() { try { intermediate_function(); } catch (const std::exception e) { std::cerr Caught exception: e.what() std::endl; print_stacktrace(); } return 0; }8.2 使用IDE或调试器在开发环境中如Visual Studio、CLion、VS Code配合GDB/LLDB你可以设置“在抛出异常时中断”。这样当异常被抛出但尚未被捕获时调试器会自动暂停程序并显示完整的调用栈和局部变量这是定位问题最有效的方法。GDBcatch throw命令可以捕获任何异常的抛出。LLDBbreakpoint set -E c或breakpoint set -E objc。Visual Studio在“异常设置”窗口中勾选相应的C异常类型。8.3 常见异常排查速查表异常类型常见触发原因排查思路std::bad_alloc内存不足内存碎片超大分配请求。1. 检查分配大小是否合理。2. 使用工具如Valgrind、AddressSanitizer检查内存泄漏。3. 分析内存使用模式考虑使用内存池或调整分配策略。4. 捕获异常并实现降级策略。std::out_of_range访问vector::at(),string::at(),array::at()等越界。1. 检查索引/迭代器值。2. 在访问前使用.size()进行验证。3. 使用调试器查看容器当时的大小和索引值。std::invalid_argument传递给函数的参数值非法如负数传给要求正数的函数。1. 查看异常信息中的参数值。2. 检查函数调用处的参数来源。3. 在函数入口处添加参数验证断言或抛出更早的异常。std::logic_error程序内部状态不一致违反类的不变式。1. 仔细审查抛出异常处的代码逻辑。2. 检查对象的状态是否在所有操作后都保持有效。3. 使用断言assert在调试版本中提前捕获逻辑错误。std::runtime_error/std::system_error文件打开失败、网络连接断开、系统调用错误。1. 检查errno或GetLastError()Windows获取系统错误码。2. 检查文件路径、权限、资源可用性。3. 确保正确关闭了文件描述符/句柄。std::bad_castdynamic_cast对引用类型转换失败。1. 确认基类指针/引用实际指向的对象类型。2. 检查类层次结构确保目标类型是源类型的公有派生类。3. 考虑使用typeid进行运行时类型检查。std::bad_typeid对空指针解引用进行typeid操作。1. 检查指针是否为空。2. 确保typeid操作的对象是多态类型有虚函数。8.4 关于异常与性能的迷思很多人担心异常处理会影响性能。确实与不使用异常的错误处理方式如返回错误码相比异常在“正常路径”不抛出异常上通常有接近零的开销。主要的开销发生在抛出和捕获异常时因为需要构造异常对象、展开栈、查找匹配的catch块。这个过程比函数返回要慢得多。因此关键原则是异常应用于处理“异常”情况即那些不常发生、但一旦发生就需要跳出当前执行流的错误。不要用异常来控制正常的程序流程比如在循环中频繁地抛出和捕获异常。对于可以预期且频繁发生的“错误”如“文件未找到”在交互式程序中可能很常见使用错误码或std::optional等可能更合适。现代编译器和标准库实现已经对异常处理做了大量优化。在大多数应用场景下正确使用异常带来的代码清晰度和安全性收益远大于其性能开销。只有在性能极其敏感的代码段如内层循环才需要考虑禁用异常使用编译器标志如-fno-exceptions或使用其他错误处理机制。但请注意禁用异常会使得标准库的许多部分如new、STL容器无法正常工作需要非常小心。