错误和异常处理(现代 C++)

在现代 C++ 中,大多数情况下报告和处理逻辑错误和运行时错误的首选方式都是使用异常。 当堆栈可能包含一些在可检测错误的函数和具有上下文来了解如何解决该错误的函数间的函数调用时,更是如此。 异常提供正式且定义完善的代码检测错误方法,以在调用堆栈上向上传递信息。

程序错误通常分为两类:第一类为由编程错误导致的逻辑错误,例如,“索引超出范围”错误。第二类为超出程序员控制的运行时错误,例如“网络服务不可用”错误。 在 C 风格编程和 COM 中,通过返回代表错误代码或特殊函数状态代码的值或者通过设置调用方可在每次调用函数后检索以确定是否已报告错误的全局变量管理错误报告。 例如,COM 编程使用 HRESULT 返回值来将错误传递给调用方,而 Win32 API 使用 GetLastError 函数来检索由调用堆栈报告的上一个错误。 在这两种情况下,由调用方识别代码并相应地作出回应。 如果调用方未显式处理错误代码,则程序可能在不发出任何警告的情况下崩溃,或者继续使用错误数据执行并产生错误结果。

现代 C++ 更趋向于使用异常的原因如下:

  • 异常强制调用代码识别错误条件并对其进行处理。 未经处理的异常会中止程序执行。

  • 异常跳转到能够处理错误的调用堆栈的点。 中间函数可导致异常传播。 它们不必与其他层相协调。

  • 在引发异常后,异常堆栈展开结构基于显式定义的规则销毁处于范围内的所有对象。

  • 异常将介于检测错误和处理错误之间的代码完全分离。

以下简化的示例演示了在 C++ 中用于引发和捕获异常的必要语法。

 
#include <stdexcept>
#include <limits>
#include <iostream>
 
using namespace std;
class MyClass
{
public:
   void MyFunc(char c)
   {
      if(c < numeric_limits<char>::max())
         throw invalid_argument("MyFunc argument too large.");
      //...
   }
};

int main()
{
   try
   {
      MyFunc(256); //cause an exception to throw
   }
 
   catch(invalid_argument& e)
   {
      cerr << e.what() << endl;
      return -1;
   }
   //...
   return 0;
}

C++ 中的异常与语言(如 C# 和 Java)中的异常类似。 在 try 块中,如果引发异常,则将由类型与异常匹配的第一个关联 catch 块捕获。 换言之,执行从 throw 语句跳转到 catch 语句。 如果未找到可用的 catch 块,则调用 std::terminate,然后程序退出。 在 C++ 中,可能引发任意类型,但建议引发从 std::exception 直接或间接派生的类型。 在前面的示例中,异常类型“invalid_argument”<stdexcept> 标头文件的标准库中定义。 如果引发异常,C++ 不提供并且也不需要 finally 块来确保释放所有资源。 “获取资源即初始化”(RAII) 惯用法使用智能指针为资源清理提供必需的功能。 有关详细信息,请参阅如何:设计异常安全性。 有关 C++ 堆栈展开机制的信息,请参见 C++ 中的异常和堆栈展开

基本原则

任何编程语言都需可靠的错误处理。 虽然异常可以提供好几种功能,支持好的错误处理方式,但不会为您代劳所有工作。 若要了解异常机制的优点,请您在设计代码时使用异常。

  • 使用断言来检查不应发生的错误。 使用异常检查可能会出现的错误,例如有关公共函数参数的输入验证错误。 有关详细信息,请参见题目为 **Exceptions vs. Assertions(异常与断言)**的一节。

  • 当一个或多个干扰函数调用将处理错误的代码与检测错误的代码分隔时,请使用异常。 在处理错误的代码紧密耦合到检测到错误的代码的性能关键循环中,考虑是否使用错误代码。 有关何时不使用异常的更多信息,请参见 When Not to Use Exceptions。

  • 对于可能引发或传播异常的每个功能,请提供三种异常保证之一:增强保证、基本保证或 nothrow (noexcept) 保证。 有关详细信息,请参阅如何:设计异常安全性

  • 通过值引发异常,通过引用来捕获这些异常。 请勿捕捉无法处理的内容。 有关详细信息,请参阅引发和捕获异常的 (C++) 准则

  • 不要使用在 C++11 中弃用的异常规范。 有关详细信息,请参见题目为 **Exception specifications and noexcept(异常规范和 noexcept)**的一节。

  • 如果适用,请使用标准库异常类型。 从异常类层次结构派生自定义异常类型。 有关详细信息,请参阅如何:使用标准库异常对象

  • 不允许从析构函数或内存释放功能逃离的异常。

异常和性能

如果未引发异常,异常结构的性能开销非常最小。 如果引发了异常,则堆栈遍历和展开的成本大致等于函数调用的成本。 需要附加数据结构以在输入 try 块后跟踪调用堆栈,并且还需要附加说明以在引发异常时展开堆栈。 但是,在大多数情况下,性能和内存占有量的成本并不重要。 只有在特定内存约束的系统,或在可能会定期产生错误、且用于处理错误的代码紧密耦合到报告错误的代码的性能关键循环中,性能异常的不利影响才可能很重要。 在任何情况下,均不可能在不进行分析和衡量的情况下知道异常的实际开销。 在这些少数情况下,当成本很重要的时候,可以权衡它会增加的正确性、更简单的可维护性和一个精心设计的异常策略提供的其他好处。

异常与断言

异常和断言是检测程序中运行时错误的两个不同的机制。 如果您的所有代码都是正确的,那么对于开发中绝不应为 true 的条件,请使用断言进行测试。 无法使用异常来处理此类错误,因为错误表明需要确认代码的某部分,但不代表在运行时程序需从某状况中恢复。 断言停止执行该语句,以便您可以检查调试器中的程序状态;异常继续从第一个正确 catch 处理程序开始执行。 使用异常检查运行时可能会出现的错误条件(即使您的代码是正确的),例如“未找到文件”或“在内存不足”。您可能希望从这些情况恢复,即使恢复仅向日志输出一条消息就结束程序。 请始终使用异常检查公共函数的参数。 即使您的功能没有错误,但也可能不能完全控制用户传递给它的参数。

C++ 异常与 Windows SEH 异常

C 和 C++ 程序两者都可以在 Windows 操作系统中使用结构化异常处理 (SEH) 机制。 SEH 的概念类似于 C++ 异常的概念,除了 SEH 使用了 __try、__except 和 __finally 构造而不是 try 和 catch。 在 Visual C++ 中,为 SEH 实现 C++ 异常。 但是,当您编写 C++ 代码时,请使用 C++ 异常语法。

有关 SEH 的更多信息,请参见 结构化异常处理 (C/C++)

异常规范和 noexcept

异常规范引入 C++ 中作为一种指定函数可能引发异常的方法。 但是,因为异常规范在实践中被证明存在问题,所以在 C++11 标准草案中已弃用。 除了 throw(),建议您不要使用其他的异常规范,这表示所有异常都要引发。 如果必须使用类型 throw(type) 的异常规范,请注意 Visual C++ 在某些方面偏离标准。 有关详细信息,请参阅异常规范。 noexcept 说明符在 C++11 中引入为 throw() 的首选备用。

请参见

概念

如何:异常和非异常代码之间的接口

其他资源

欢迎回到 C++(现代 C++)

C++ 语言参考

C++ 标准库参考