Skip to content

14.2 基本异常处理

By Alex on October 4th, 2008 | last modified by Alex on January 23rd, 2020

翻译by dashjay 2020.07.13

在之前的课程 为什么需要异常中,我们讨论了关于使用返回值状态码为何会使得你的控制流和错误处理被混合,使得两者相互约束。异常在C++中的实现使用了三个关键词,来相互连接:throw, try, catch

抛出异常

在现实生活中,我们使用信号来记录已经发生的特定的事情。例如在美国足球比赛中,如果一个运动员犯规了,裁判将会拿出一个flag并且吹哨表示游戏暂停。惩罚就是紧接着评估和执行。惩罚被执行后,游戏照旧继续。

C++ 中,一个抛出语句被用来发送一个信号表示一个异常或者错误已经发生(可以认为是抛出了一个惩罚的flag)。发出信号表示一个异常已经发生,者通常被叫做抛出一个异常。

为了使用一个异常一句,简单的使用 throw 关键词,紧跟着一个你希望使用的任何类型的值来通知一个错误已经发生,通常这和值将会是一个错误码,一个问题的描述,或者一个自定义异常类。

这里是一些例子:

throw -1; // throw a literal integer value
throw ENUM_INVALID_INDEX; // throw an enum value
throw "Can not take square root of negative number"; // throw a literal C-style (const char*) string
throw dX; // throw a double variable that was previously defined
throw MyException("Fatal Error"); // Throw an object of class MyException

这里的每一个语句都当做一个信号,表示某种类型的需要被解决的问题已经发生了。

寻找异常

抛出异常只是异常处理进程的一部分。让我们回到美国足球的比喻中:一旦裁判抛出一个一个惩罚flag,接下来会发生什么?参赛者们主要到一个惩罚发生,并且停止游戏。一个普通的足球比赛流程中断。

在C++中,我们用 try 关键词来定义一个语句块(被叫做 try 语句块),try 语句块作为一个观察者,寻找任何类型被抛出的异常在 try 语句的block中。

Here’s an example of a try block:

try
{
    // Statements that may throw exceptions you want to handle go here
    // 到那儿,可能抛出你需要处理的异常的语句。
    throw -1; // here's a trivial throw statement
              // 一个常识抛出的语句
}

注意,try 语句块没有定义如何处理异常。他仅仅告诉程序,“嘿,如果任何语句在这个 try 语句块中抛出,抓住它!”。

异常处理

最后,美国足球的比喻:在惩罚被调用,游戏已经停止,裁判评估惩罚并且执行它。换句话说,惩罚必须被处理在继续游戏之前。

事实上,异常处理是 catch 语句块的工作。catch 关键词被用来定义一个语句块(被叫做 catch 语句块)处理单个数据类型的的异常。

这有一个 catch 语句块的例子,它会捕获一个整型异常:

catch (int x)
{
    // Handle an exception of type int here
    // 处理一个整型的异常
    std::cerr << "We caught an int exception with value" << x << '\n';
}

try 语句块和 catch 语句块一起工作—— 一个 try 语句块检测 try 语句块中的任何语句抛出的异常,并且发送它们到,合适的 catch 语句块来进行处理。一个 try 语句块必须有至少一个 catch 语句块,紧跟着 try 语句,也许有很多个捕获语句快按顺序排列。

一旦一个异常在 try 语句中被捕获,并且发送到一个 catch 语句块来处理,异常被认为处理,并且在 catch 语句后执行将会像往常一样继续。

捕获参数就像函数参数那样工作,参数在后续的 try 语句块中可用。基础类型的异常可以被捕获以值的形式,但是非基础类型的异常应该被捕获,以常引用的方式,来避免不必要的拷贝。

就像用函数那样,如果参数没有在语句中被使用,变量名可以被省略

catch (double) // note: no variable name since we don't use it in the catch block below
               // 注意:无变量名,因为我们不会再catch语句块中使用它
{
    // Handle exception of type double here
    // 处理 double 类型的异常
    std::cerr << "We caught an exception of type double" << '\n';
}

这可以防止编译器做有关未使用变量的警告。

抛出(throw),try,捕获(catch)

这有一整个程序,使用了 throw, try 和许多 catch 语句块。

#include <iostream>
#include <string>

int main()
{
    try
    {
        // Statements that may throw exceptions you want to handle go here
        throw -1; // here's a trivial example
    }
    catch (int x)
    {
        // Any exceptions of type int thrown within the above try block get sent here
        std::cerr << "We caught an int exception with value: " << x << '\n';
    }
    catch (double) // no variable name since we don't use the exception itself in the catch block below
    {
        // Any exceptions of type double thrown within the above try block get sent here
        std::cerr << "We caught an exception of type double" << '\n';
    }
    catch (const std::string &str) // catch classes by const reference
    {
        // Any exceptions of type std::string thrown within the above try block get sent here
        std::cerr << "We caught an exception of type std::string" << '\n';
    }

    std::cout << "Continuing on our merry way\n";

    return 0;
}

运行以上的 try/catch 语句块将会产生如下结果:

We caught an int exception with value -1
Continuing on our merry way

一个抛出语句被用来抛出一个异常,通过 -1 这个值,类型为 intthrow 语句会被紧接着的封闭的 try 语句块捕获,并且发送到合适的处理整型异常的 catch 语句块。这个 catch 语句块打印了合适的错误信息。

一旦异常被处理,程序就会从 catch 语句块结束的地方开始正常运行,并且打印 “Continuing on our merry way”。

再复习异常处理

异常处理实际上非常简单,下面两段话覆盖了大多数你需要记得的有关异常的事情:

当一个异常被使用 throw 抛出,程序的执行会立即跳到最近的 try 语句块(向上传播堆栈,如果有必要找到一个封闭的 try 语句块——我们将在下节课讨论更加详细的内容)。如果任何异常函数可以处理之前 try 语句快抛出的异常,那个函数将会被执行,异常也会被认为处理了。

如果没有合适的处理函数存在,执行的程序会跳出到下一个 try 闭合语句,如果没有合适的 catch 语句可以被找到在程序结束前,程序将会带着异常错误失败。

注意,编译器不会执行一个隐式转化 (implicit conversions) 或者升级 (promotions) 当使用 catch 语句来捕获异常时!例如,一个 char 类型的异常将不会匹配一个 int 类型的 catch 语句块。一个 int 异常将不会匹配一个 float 类型的 catch 语句块。然而,从派生类到父类之一将会执行。

这就是全部,接下来的章节将会尽量展示所有这些原则的例子。

异常被立即处理

这是一个短小的程序,展示了异常如何被立刻处理:

#include <iostream>

int main()
{
    try
    {
        throw 4.5; // throw exception of type double
        std::cout << "This never prints\n";
    }
    catch(double x) // handle exception of type double
    {
        std::cerr << "We caught a double of value: " << x << '\n';
    }

    return 0;
}

这个程序非常简单。这就是具体发生的的事情:抛出语句是第一执行的语句 —— 这引起了一个 double 类型的异常被抛出。执行流程一颗移动到最近的 try 语句块闭合处,也是这个程序中唯一的 try block。catch 语句将会紧接着检查是否有 handler 匹配。我们的异常就是 double 类型的。而且我们正在寻找一个 double 类型的 catch 语句,我们刚好有一个,紧接着它就会执行。

因此,这个程序输出如下:

We caught a double of value: 4.5

注意到 "this never prints" 是从没被打印的,因为异常造成执行路径立刻跳到 double 的异常处理。

一个更加真实的例子

让我们看一个不是那么理论的例子:

#include "math.h" // for sqrt() function
#include <iostream>

int main()
{
    std::cout << "Enter a number: ";
    double x;
    std::cin >> x;

    try // Look for exceptions that occur within try block and route to attached catch block(s)
    {
        // If the user entered a negative number, this is an error condition
        if (x < 0.0)
            throw "Can not take sqrt of negative number"; // throw exception of type const char*

        // Otherwise, print the answer
        std::cout << "The sqrt of " << x << " is " << sqrt(x) << '\n';
    }
    catch (const char* exception) // catch exceptions of type const char*
    {
        std::cerr << "Error: " << exception << '\n';
    }
}

In this code, the user is asked to enter a number. If they enter a positive number, the if statement does not execute, no exception is thrown, and the square root of the number is printed. Because no exception is thrown in this case, the code inside the catch block never executes. The result is something like this:

在这段代码中,用户被要求输入一个数字,如果他们输入一个正数,那么 if 语句不会执行,没有异常抛出,并且输入数字的平方根将会被打印。因为没有异常在这个例子中被抛出,catch 语句块中的代码从不会执行,结果如下:

Enter a number: 9
The sqrt of 9 is 3

If the user enters a negative number, we throw an exception of type const char*. Because we’re within a try block and a matching exception handler is found, control immediately transfers to the const char* exception handler. The result is:

Enter a number: -4 Error: Can not take sqrt of negative number

By now, you should be getting the basic idea behind exceptions. In the next lesson, we’ll do quite a few more examples to show how flexible exceptions are.

What catch blocks typically do

If an exception is routed to a catch block, it is considered “handled” even if the catch block is empty. However, typically you’ll want your catch blocks to do something useful. There are three common things that catch blocks do when they catch an exception:

First, catch blocks may print an error (either to the console, or a log file).

Second, catch blocks may return a value or error code back to the caller.

Third, a catch block may throw another exception. Because the catch block is outside of the try block, the newly thrown exception in this case is not handled by the preceding try block -- it’s handled by the next enclosing try block.