章节目录

基本异常处理

本节阅读量:

在上一节中,我们讨论了使用返回码会导致控制流和错误流混在一起,从而让代码变得混乱。本节将讲解异常的基本用法。

C++中的异常通过三个相互关联的关键字实现:throw、try和catch。


引发异常

在现实生活中,我们经常使用信号来标记特定事件的发生。例如,在足球比赛中,如果一名球员犯规,裁判会吹口哨暂停比赛,然后评估并执行处罚。一旦处罚处理完毕,比赛通常会恢复正常。

在C++中,throw语句用于发出异常信号。发出异常信号的行为通常也被称为引发异常。

要使用throw语句,只需写出throw关键字,后面跟上一个表示错误的值,该值可以是任意数据类型。通常,这个值会是错误码、问题描述或自定义异常类。

下面是一些示例:

1
2
3
4
5
throw -1; // throw 一个 int 值
throw ENUM_INVALID_INDEX; // throw 一个 枚举值
throw "Can not take square root of negative number"; // throw 一个C样式字符串 (const char*)
throw dX; // throw 一个定义的变量
throw MyException("Fatal Error"); // Throw 一个 MyException 类的对象

这些语句都充当信号,表示发生了某种需要处理的问题。


监听异常发生

引发异常只是异常处理过程的一部分。让我们回到足球类比:一旦裁判吹响口哨,接下来会发生什么?球员会意识到需要停止比赛,足球比赛的正常进行被打断了。

在C++中,我们使用try关键字来定义语句块(称为try块)。try块充当观察者,用来监视try块中任何语句引发的异常。

下面是try块的示例:

1
2
3
4
5
try
{
    // 任何可能会引发异常的语句,在try块内被监听
    throw -1; // 这是一个简单的throw语句
}

注意,try块并不定义如何处理异常。它只是告诉程序:“如果这个try块中的任何语句抛出异常,请捕获它!”


处理异常

最后,到了足球类比的最后一步:犯规被判定且比赛停止后,裁判会评估处罚并执行它。换句话说,在恢复正常比赛之前,必须先处理好处罚。

在C++中,处理异常是catch块的工作。catch关键字用于定义代码块(称为catch块),该代码块处理某一种数据类型的异常。

下面是捕获整数异常的catch块的示例:

1
2
3
4
5
catch (int x)
{
    // 这里处理一个 int 类型的异常
    std::cerr << "We caught an int exception with value" << x << '\n';
}

try块和catch块协同工作——try块检测其中语句抛出的异常,并将它们路由到类型匹配的catch块进行处理。try块后面必须紧跟至少一个catch块,但可以按顺序列出多个catch块。

一旦异常被try块捕获并路由到匹配的catch块进行处理,就认为该异常已被处理。匹配的catch块执行完后,程序会恢复正常执行,从最后一个catch块之后的第一条语句继续。

catch参数的工作方式与函数参数类似,参数在对应的catch块中可用。基本类型的异常可以按值捕获,但非基本类型的异常应该按常量引用捕获,以避免不必要的复制(在某些情况下还能防止对象切片)。

与函数一样,如果参数不打算在catch块中使用变量名,则可以省略:

1
2
3
4
5
catch (double) // 注: 没有变量名,因为下方没有使用
{
    // 处理 double 类型的异常
    std::cerr << "We caught an exception of type double\n";
}

这有助于防止编译器对未使用的变量发出警告。

此外,异常不会进行类型转换(因此int异常不会被转换为double来匹配具有double参数的catch块)。


try、throw、catch

下面是一个使用throw、try和多个catch块的完整程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <string>

int main()
{
    try
    {
        // 抛出异常
        throw -1; // 这是一个简单的例子
    }
    catch (double) // 注: 没有变量名,因为下方没有使用
    {
        // try块内的double异常会在这里处理
        std::cerr << "We caught an exception of type double\n";
    }
    catch (int x)
    {
        // try块内的int异常会在这里处理
        std::cerr << "We caught an int exception with value: " << x << '\n';
    }
    catch (const std::string&) // const引用 异常捕获
    {
        // try块内的std::string异常会在这里处理
        std::cerr << "We caught an exception of type std::string\n";
    }

    // 异常处理完后,会在这里接着执行
    std::cout << "Continuing on our merry way\n";

    return 0;
}

在作者的计算机上,运行上述try/catch块会产生以下结果:

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

throw语句引发了一个值为-1的异常,该异常属于int类型。随后,异常被try块捕获,并路由到处理int类型异常的匹配catch块。该catch块打印了相应的错误消息。

一旦异常被处理,程序在catch块后继续正常运行,并打印“Continuing on our merry way”。


异常处理概括

异常处理实际上相当简单,下面两段涵盖了您需要记住的大部分内容:

当引发异常时(使用throw),正在运行的程序会查找最近的try块(如果需要查找try块,则会沿调用栈向上查找——我们将在下一课更详细地讨论这一点),并查看附加到该try块的catch处理程序是否可以处理这种类型的异常。如果可以,执行流会跳到对应catch块的开头,该异常被认为已处理。

如果最近的try块中不存在合适的catch处理程序,程序会继续查看更外层try块中的catch块。如果在程序结束前仍找不到合适的捕获处理程序,程序将失败,并产生运行时异常错误。

请注意,将异常与catch块匹配时,程序不会执行隐式类型转换或提升!例如,char异常不会匹配int catch块,int异常也不会匹配float catch块。然而,会执行从派生类到其父类的转换。

这就是异常的核心内容。本章的其余部分将通过示例展示这些原则在实践中的用法。


立即处理异常

下面是一个简短的程序,演示如何立即处理异常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
    try
    {
        throw 4.5; // 抛出类型为 double 的异常
        std::cout << "This never prints\n";
    }
    catch (double x) // 处理类型为 double 的异常
    {
        std::cerr << "We caught a double of value: " << x << '\n';
    }

    return 0;
}

这个程序非常简单。执行过程如下:throw语句是执行的第一条语句——它会抛出double类型的异常。执行流立即转移到最近的try块,也就是该程序中唯一的try块。然后程序检查catch处理程序,看看是否有匹配项。我们的异常是double类型,因此会寻找double类型的catch处理程序。这里存在匹配项,所以它会执行。

因此,该程序的结果如下:

1
We caught a double of value: 4.5

请注意,“This never prints”永远不会被打印,因为异常会导致执行路径立即跳转到double的catch处理程序。


一个更现实的例子

让我们看一个更实际的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <cmath> // for sqrt()
#include <iostream>

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

    try // 监听try块内的异常,并将其发送到catch块
    {
        // 如果用户输入小于0, 那么肯定是有问题
        if (x < 0.0)
            throw "Can not take sqrt of negative number"; // 抛出类型为 const char* 的异常

        // 否则, 打印计算结果
        std::cout << "The sqrt of " << x << " is " << std::sqrt(x) << '\n';
    }
    catch (const char* exception) // 处理类型为 const char* 的异常
    {
        std::cerr << "Error: " << exception << '\n';
    }
}

在此代码中,程序要求用户输入数字。如果输入正数,则不会执行if语句中的throw,不会引发异常,并会打印数字的平方根。因为这种情况下不会引发异常,所以catch块内的代码不会执行。结果如下:

1
2
Enter a number: 9
The sqrt of 9 is 3

如果用户输入负数,则会抛出const char*类型的异常。因为我们处于try块中,并且找到了匹配的catch处理程序,所以控制流会立即转移到const char*异常处理程序。结果是:

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

现在,您应该已经了解了异常背后的基本思想。在下一课中,我们将再举几个例子来展示异常的灵活性。


catch块通常做什么

如果异常被路由到catch块,即使catch块为空,也会被视为“已处理”。不过,通常我们希望catch块执行一些有用的操作。当catch块捕获异常时,通常会做以下四件事:

首先,catch块可以打印错误(到控制台或日志文件),然后允许函数继续执行。

其次,catch块可以将值或错误码返回给调用者。

第三,catch块可能会引发另一个异常。因为catch块位于try块之外,所以在这种情况下新抛出的异常不会由当前try块处理——它会由更外层的try块处理。

第四,main()中的catch块可以用于捕获致命错误并以干净的方式终止程序。


27.0 为什么需要异常机制

上一节

27.2 异常、函数和堆栈展开

下一节