C++语句 - 【9】异常处理(补充)

本文深入探讨了C++的异常处理机制,包括try、throw和catch语句的使用,异常检测和处理,以及异常类的层次结构。强调了在代码中预测和处理异常的重要性,推荐使用类型安全的异常处理,并指出应避免使用catch(...)处理程序,除非在必要时进行错误记录和清理工作。此外,文章提到了异常规范的弃用,以及在混合C和C++异常时的注意事项,强调了异常安全编程的重要性。
摘要由CSDN通过智能技术生成

5.9 异常处理语句

异常是一个可能超出程序的控制范围的错误条件,它会阻止程序继续沿其常规执行路径执行。 某些操作(包括对象创建、文件输入/输出以及从其他模块中进行的函数调用)都可能是异常的来源,设置在你的程序正常运行时也是如此。 可靠代码可预见并处理异常。

若要检测单个程序或模块内的逻辑错误,请使用断言而不是异常。

对于大多数 C++ 程序,你应使用类型安全的 C++ 异常处理,该处理可确保在堆栈展开过程中调用对象析构函数。

C++ 语言为引发和捕获异常提供内置支持。 

异常处理机制为程序中的异常检测和异常处理两部分的协作提供支持。在C++语言中,异常处理包括:

1)异常检测部分使用throw表达式来表示其遇到的无法处理的问题。

2)try语句块用来进行异常处理。该语句块以关键字try开始,并以一个或者多个catch子句结束。

3)一套异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息。

try {
    program-statement
}  catch (exception-declaration){
     handler-statement
}   catch (exception-declaration){
     handler-statement
}   //...

5.9.1 try、throw和catch语句

若要在 C++ 中实现异常处理,可以使用 try、throw 和 catch 表达式。

首先,使用 try 块将可能引发异常的一个或多个语句封闭起来。

throw 表达式发出信号,异常条件(通常是错误)已在 try 块中发生。 你可以使用任何类型的对象作为 throw 表达式的操作数。该对象一般用于传达有关错误的信息。大多数情况下,建议你使用std::exception类或标准库中定义的派生类之一。 如果其中的类不合适,建议你从std::exception派生自己的异常类。

若要处理可能引发的异常,请在try块之后立即实现一个或多个 catch 块。 每个 catch 块指定它能处理的异常类型。

以下示例将显示 try 块及其处理程序。 假设 GetNetworkResource() 通过网络连接获取数据,并且两个异常类型是从std::exception派生的用户定义的类。请注意,异常由 catch 语句中的 const 引用捕获。 我们建议你通过值引发异常并通过常数引用将其捕获。

MyData md;
try {
   // Code that could throw an exception
   md = GetNetworkResource();
}
catch (const networkIOException& e) {
   // Code that executes when an exception of type
   // networkIOException is thrown in the try block
   // ...
   // Log error message in the exception object
   cerr << e.what();
}
catch (const myDataFormatException& e) {
   // Code that handles another exception type
   // ...
   cerr << e.what();
}

// The following syntax shows a throw expression
MyData GetNetworkResource()
{
   // ...
   if (IOSuccess == false)
      throw networkIOException("Unable to connect");
   // ...
   if (readError)
      throw myDataFormatException("Format error"); 
   // ...
}

try 子句后的代码是代码的受保护部分。 throw 表达式将引发(即引起)异常。 catch 子句后的代码块是异常处理程序。 如果 throw 和 catch 表达式中的类型兼容,该处理程序将捕获引发的异常。 如果 catch 语句指定省略号 (...) 而非类型,catch 块将处理每种类型的异常。 当你使用 /EHa 选项编译时,异常可包括 C 结构化异常和系统生成或应用程序生成的异步异常,例如内存保护、被零除和浮点冲突。 由于 catch 块按编程顺序处理以查找匹配类型,所以尽量不要使用省略号处理程序来处理关联的 try 块。 请谨慎使用 catch(...);除非 catch 块知道如何处理捕获的特定异常,否则禁止程序继续执行。 catch(...) 块一般用于在程序停止执行前记录错误和执行特殊的清理工作。

没有操作数的 throw 表达式将重新引发当前正在处理的异常。我们建议在重新引发异常时采用该形式,是因为这将保留原始异常的多态类型信息。 此类表达式只应在 catch 处理程序中或从 catch 处理程序调用的函数中使用。 重新引发的异常对象是原始异常对象,而不是副本。

try {
   throw CSomeOtherException();
}
catch(...) {
   // Catch all exceptions – dangerous!!!
   // Respond (perhaps only partially) to the exception, then
   // re-throw to pass the exception to some other handler
   // ...
   throw;
}

5.9.2 catch块的计算方式

虽然通常建议您引发派生自 std::exception 的类型,但 C++ 使您能够引发任何类型的异常。可以通过指定与引发的异常相同的类型的 catch 处理程序或通过可捕获任何类型的异常的处理程序来捕获 C++ 异常。

如果引发的异常的类型是类,它还具有基类(或类),则它可由接受异常类型的基类和对异常类型的基的引用的处理程序捕获。请注意,当异常由引用捕获时,会将其绑定到实际引发的异常对象;否则,它将为一个副本(与函数的参数大致相同)。

引发异常时,将由以下类型的 catch 处理程序捕获该异常:

1)可以接受任何类型的处理程序(使用省略号语法)。

2)接受与异常对象相同的类型的处理程序;由于它是副本,因此 const 和 volatile 修饰符将被忽略。

3)接受对与异常对象相同的类型的引用的处理程序。

4)接受对与异常对象相同的类型的 const 或 volatile 形式的引用的处理程序。

5)接受与异常对象相同的类型的基类的处理程序;由于它是副本,因此 const 和 volatile 修饰符将被忽略。 基类的 catch 处理程序不得位于派生类的 catch 处理程序的前面。

6)接受对与异常对象相同的类型的基类的引用的处理程序。

7)接受与异常对象相同的类型的基类的 const 或 volatile 形式的引用的处理程序。

8)接受可通过标准指针转换规则将引发的指针对象转换为的指针的处理程序。

catch 处理程序出现的顺序是有意义的,因为给定 try 块的处理程序按它们的出现顺序进行检查。 例如,将基类的处理程序放置在派生类的处理程序的前面是错误的。 找到一个匹配的 catch 处理程序后,不会检查后续处理程序。 因此,省略号 catch 处理程序必须是其 try 块的最后一个处理程序。 例如:

// ...
try
{
    // ...
}
catch( ... )
{
    // Handle exception here.
}
// Error: the next two handlers are never examined.
catch( const char * str )
{
    cout << "Caught exception: " << str << endl;
}
catch( CExcptClass E )
{
    // Handle CExcptClass exception here.
}

5.9.3 异常规范

异常规范是在 C++11 中弃用的 C++ 语言功能。这些规范原本用来提供有关可从函数引发哪些异常的摘要信息,但在实际应用中发现这些规范存在问题。 证明确实有一定用处的一个异常规范是 throw() 规范。 例如:

void MyFunction(int i) throw();

告诉编译器函数不引发任何异常。 它相当于使用 __declspec(nothrow)。 这种用法是可选的。 在 ISO C++11 标准中,引入了 noexcept 运算符,但是目前 Visual C++ 中不支持此功能。

Visual C++ 中实现的异常规范与 ISO C++ 标准有所不同。下表总结了 Visual C++ 的异常规范实现:

异常规范

含义

throw()

函数不会引发异常。 但是,如果从标记为 throw() 函数引发异常,Visual C++ 编译器将不会调用意外处理函数(有关更多信息,请参阅unexpected (CRT) 和unexpected (<exception>))。 如果使用 throw() 标记一个函数,则 Visual C++ 编译器假定该函数不会引发 C++ 异常,并相应地生成代码。 由于 C++ 编译器可能会执行代码优化(基于函数不会引发任何 C++ 异常的假设),因此,如果函数引发异常,则程序可能无法正确执行。

throw(...)

函数可以引发异常。

throw(type)

函数可以引发 type 类型的异常。 但是,在 Visual C++ .NET 中,这被解释为 throw(...)。请参阅函数异常说明符。

如果在应用程序中使用异常处理,则一定有一个或多个函数处理引发的异常。 在引发异常的函数和处理异常的函数间调用的所有函数必须能够引发异常。

函数的引发行为基于以下因素:

1)您是否在 C 或 C++ 下编译函数。

2)您所使用的 /EH 编译器选项。

3)是否显式指定异常规范。

不允许对 C 函数使用显式异常规范。

下表总结了函数的引发行为:

功能

/EHsc

/EHs

/EHa

/EHac

C 函数

throw()

throw(...)

throw(...)

throw(...)

没有异常规范的 C++ 函数

throw(...)

throw(...)

throw(...)

throw(...)

带有 throw() 异常规范的 C++ 函数

throw()

throw()

throw(...)

throw(...)

带有 throw(...) 异常规范的 C++ 函数

throw(...)

throw(...)

throw(...)

throw(...)

带有 throw(type) 异常规范的 C++ 函数

throw(...)

throw(...)

throw(...)

throw(...)

// exception_specification.cpp
// compile with: /EHs
#include <stdio.h>

void handler() {
   printf_s("in handler\n");
}

void f1(void) throw(int) {
   printf_s("About to throw 1\n");
   if (1)
      throw 1;
}

void f5(void) throw() {
   try {
      f1();
   }
   catch(...) {
      handler();
    }
}

// invalid, doesn't handle the int exception thrown from f1()
// void f3(void) throw() {
//   f1();
// }

void __declspec(nothrow) f2(void) {
   try {
      f1();
   }
   catch(int) {
      handler();
    }
}

// only valid if compiled without /EHc 
// /EHc means assume extern "C" functions don't throw exceptions
extern "C" void f4(void);
void f4(void) {
   f1();
}

int main() {
   f2();

   try {
      f4();
   }
   catch(...) {
      printf_s("Caught exception from f4\n");
   }
   f5();
}
      1. C++ 中的异常和堆栈展开

在 C++ 异常机制中,控制从 throw 语句移至可处理引发类型的第一个 catch 语句。 在到达 catch 语句时,throw 语句和 catch 语句之间的范围内的所有自动变量将在名为“堆栈展开”的过程中被销毁。 在堆栈展开中,执行将继续,如下所示:

1)控制通过正常顺序执行到达 try 语句。 执行 try 块内的受保护部分。

2)如果执行受保护的部分的过程中未引发异常,将不会执行 try 块后面的 catch 子句。 执行将在关联的 try 块后的最后一个 catch 子句后面的语句上继续。

3)如果执行受保护部分的过程中或在受保护的部分调用的任何例程中引发异常(直接或间接),则从通过 throw 操作数创建的对象中创建异常对象。(这意味着,可能会涉及复制构造函数。)此时,编译器会在权限更高的执行上下文中查找可处理引发的类型异常的 catch 子句,或查找可以处理任何类型的异常的 catch 处理程序。 按照 catch 处理程序在 try 块后面的显示顺序检查这些处理程序。 如果未找到适当的处理程序,则检查下一个动态封闭的 try 块。 此过程将继续,直到检查最外面的封闭 try 块。

4)如果仍未找到匹配的处理程序,或者在展开过程中但在处理程序获得控制前发生异常,则调用预定义的运行时函数 terminate。 如果在引发异常后但在展开开始前发生异常,则调用 terminate。

5)如果找到匹配的 catch 处理程序,并且它通过值进行捕获,则通过复制异常对象来初始化其形参。 如果它通过引用进行捕获,则初始化参数以引用异常对象。 在初始化形参后,堆栈的展开过程将开始。 这包括对与 catch 处理程序关联的 try 块的开头和异常的引发站点之间完全构造(但尚未析构)的所有自动对象的析构。 析构按照与构造相反的顺序发生。 执行 catch 处理程序且程序会在最后一个处理程序之后(即,在不是 catch 处理程序的第一个语句或构造处)恢复执行。 控制只能通过引发的异常进入 catch 处理程序,而绝不会通过 goto 语句或 switch 语句中的 case 标签进入。

堆栈展开示例

以下示例演示引发异常时如何展开堆栈。线程执行将从 C 中的 throw 语句跳转到 main 中的 catch 语句,并在此过程中展开每个函数。 请注意创建 Dummy 对象的顺序,并且会在它们超出范围时将其销毁。 还请注意,除了包含 catch 语句的 main 之外,其他函数均未完成。 函数 A 绝不会从其对 B() 的调用返回,并且 B 绝不会从其对 C() 的调用返回。 如果取消注释 Dummy 指针和相应的 delete 语句的定义并运行程序,请注意绝不会删除该指针。 这说明了当函数不提供异常保证时会发生的情况。 有关详细信息,请参阅“如何:针对异常进行设计”。 如果注释掉 catch 语句,则可以观察当程序因未经处理的异常而终止时将发生的情况。

#include <string>
#include <iostream>
using namespace std;
 
class MyException{};
class Dummy
{
    public:
    Dummy(string s) : MyName(s) { PrintMsg("Created Dummy:"); }
    Dummy(const Dummy& other) : MyName(other.MyName){ PrintMsg("Copy created Dummy:"); }
    ~Dummy(){ PrintMsg("Destroyed Dummy:"); }
    void PrintMsg(string s) { cout << s  << MyName <<  endl; }
    string MyName; 
    int level;
};
 
 
void C(Dummy d, int i)
{ 
    cout << "Entering FunctionC" << endl;
    d.MyName = " C";
    throw MyException();   
 
    cout << "Exiting FunctionC" << endl;
}
 
void B(Dummy d, int i)
{
    cout << "Entering FunctionB" << endl;
    d.MyName = "B";
    C(d, i + 1);   
    cout << "Exiting FunctionB" << endl; 
}
 
void A(Dummy d, int i)
{ 
    cout << "Entering FunctionA" << endl;
    d.MyName = " A" ;
  //  Dummy* pd = new Dummy("new Dummy"); //Not exception safe!!!
    B(d, i + 1);
 //   delete pd; 
    cout << "Exiting FunctionA" << endl;   
}
 
 
int main()
{
    cout << "Entering main" << endl;
    try
    {
        Dummy d(" M");
        A(d,1);
    }
    catch (MyException& e)
    {
        cout << "Caught an exception of type: " << typeid(e).name() << endl;
    }
 
    cout << "Exiting main." << endl;
    char c;
    cin >> c;
}
 
/* Output:
    Entering main
    Created Dummy: M
    Copy created Dummy: M
    Entering FunctionA
    Copy created Dummy: A
    Entering FunctionB
    Copy created Dummy: B
    Entering FunctionC
    Destroyed Dummy: C
    Destroyed Dummy: B
    Destroyed Dummy: A
    Destroyed Dummy: M
    Caught an exception of type: class MyException
    Exiting main.
 
*/

5.9.5 未经处理的 C++ 异常

如果无法找到当前异常的匹配处理程序(或省略号 catch 处理程序),则调用预定义的 terminate 运行时函数。(您也可以在任意处理程序中显式调用 terminate。)terminate 的默认操作是调用 abort。 如果您希望 terminate 在退出应用程序之前调用程序中的某些其他函数,则用被调用函数的名称作为其单个参数调用 set_terminate 函数。 您可以在程序的任何点调用 set_terminate。 terminate 例程总是调用指定为 set_terminate 的参数的最后一个函数。

以下示例引发 char * 异常,但不包含用于捕获类型 char * 的异常的指定处理程序。 对 set_terminate 的调用指示 terminate 调用 term_func。

// exceptions_Unhandled_Exceptions.cpp
// compile with: /EHsc
#include <iostream>
using namespace std;
void term_func() {
   cout << "term_func was called by terminate." << endl;
   exit( -1 );
}
int main() {
   try
   {
      set_terminate( term_func );
      throw "Out of memory!"; // No catch handler for this exception
   }
   catch( int )
   {
      cout << "Integer exception raised." << endl;
   }
   return 0;
}
输出 :
term_func was called by terminate.

term_func 函数最好是通过调用 exit 来终止程序或当前线程。 如果它没有这样做,而是返回到其调用方,则调用 abort。

5.9.10 混合 C(结构化)和 C++ 异常

若要编写可移植性更高的代码,建议不要在 C++ 程序中使用结构化异常处理。 但是,您有时可能希望使用 /EHa 进行编译并将结构化异常和 C++ 源代码组合在一起,并且需要用于处理这两种异常的某个设备。 由于结构化异常处理程序没有对象或类型化异常的概念,因此它无法处理 C++ 代码引发的异常;但是,C++ catch 处理程序可以处理结构化异常。 因此,C 编译器不接受 C++ 异常处理语法(TRY、throw、catch),但 C++ 编译器支持结构化异常处理语法(__try、__except、__finally)。

有关将结构化异常作为 C++ 异常处理的信息,请参阅 _set_se_translator。

如果将结构化异常和 C++ 异常混合,请注意以下几点:

1)不能在同一函数中混合 C++ 异常和结构化异常。

2)始终执行终止处理程序(__finally 块),甚至在引发异常后的展开过程中也是如此。

3)C++ 异常处理可以捕获并保留使用 /EH 编译器选项(此选项启用展开语义)编译的所有模块中的展开语义。

4)可以存在一些不为所有对象调用析构函数的情况。 例如,如果在尝试通过未初始化的函数指针进行函数调用时发生结构化异常,并且该函数将调用前构造的对象用作参数,则在堆栈展开过程中将不会调用这些对象的析构函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值