章节目录

为什么需要异常机制

本节阅读量:

在很多实际项目中,禁止使用异常机制。如果您参与的项目未使用异常机制,可以跳过本章的学习。

关于错误处理,我们已经讨论过assert()、std::cerr和exit()。不过,还有一个主题之前暂时搁置了:异常(exception)处理。


函数返回码的不足

在编写可复用代码时,错误处理是必要的。处理潜在错误最常见的方法之一是使用返回码。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <string_view>

int findFirstChar(std::string_view string, char ch)
{
    // 遍历 string 中每一个字符
    for (std::size_t index{ 0 }; index < string.length(); ++index)
        // 如果string中的字符匹配 ch, 返回对应的 index
        if (string[index] == ch)
            return index;

    // 如果没找到,返回 -1
    return -1;
}

此函数返回字符串中第一个与ch匹配的字符位置。如果找不到匹配字符,函数会返回-1,表示未找到。

这种方法的主要优点是非常简单。然而,返回码也有许多缺点,在稍微复杂一些的场景中,这些缺点很快就会显现出来:

首先,返回值可能是神秘的——如果函数返回-1,它是试图指示错误,还是这实际上是一个有效的返回值?如果不深入阅读函数或参考文档,通常很难判断。

其次,函数只能返回一个值,那么当您需要同时返回函数结果和可能的错误码时该怎么办?考虑以下函数:

1
2
3
4
double divide(int x, int y)
{
    return static_cast<double>(x)/y;
}

该函数明显需要错误处理,因为如果用户为参数y传入0,程序将崩溃。然而,它还需要返回x/y的结果。它如何同时做到这两点?最常见的答案是,将结果或错误状态通过引用参数传回,但这会让代码变得难看,也不太方便使用。例如:

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

double divide(int x, int y, bool& outSuccess)
{
    if (y == 0)
    {
        outSuccess = false;
        return 0.0;
    }

    outSuccess = true;
    return static_cast<double>(x)/y;
}

int main()
{
    bool success {}; // 必须传递一个 bool 来看是否计算成功
    double result { divide(5, 3, success) };

    if (!success) // 使用结果前必须检查下
        std::cerr << "An error occurred" << std::endl;
    else
        std::cout << "The answer is " << result << '\n';
}

第三,在许多操作都可能出错的代码序列中,必须不断检查错误代码。考虑以下代码片段,它会解析文本文件中应该存在的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    std::ifstream setupIni { "setup.ini" }; // 打开 setup.ini
    // 如果文件无法打开 (例如, 可能文件不存在) 返回一个错误码
    if (!setupIni)
        return ERROR_OPENING_FILE;

    // 现在,从文件中读取一些值
    if (!readIntegerFromFile(setupIni, m_firstParameter)) // 尝试读一个 int
        return ERROR_READING_VALUE; // 如果读失败,返回一个错误码

    if (!readDoubleFromFile(setupIni, m_secondParameter)) // 尝试读一个 double
        return ERROR_READING_VALUE;

    if (!readFloatFromFile(setupIni, m_thirdParameter)) // 尝试读一个 float
        return ERROR_READING_VALUE;

我们还没有介绍文件访问,因此如果您不理解上面的工作原理,请不要担心——只需注意,每个调用都需要进行错误检查,并在失败时返回给调用者。现在想象一下,如果有20个不同类型的参数,您实际上要检查错误并返回20次ERROR_READING_VALUE!所有这些错误检查和返回值,会让函数真正想做的事情更难看清。

第四,返回码无法很好地与构造函数配合。如果您正在创建一个对象,而构造函数中发生了灾难性错误,会发生什么?构造函数没有返回类型可用于传回状态,而通过引用参数传回状态既混乱,又必须显式检查。此外,即使发生了错误,对象仍然会被创建,之后还必须处理它。

最后,当错误代码返回给调用者时,调用者并不总是能够处理该错误。如果调用方不想处理错误,要么必须忽略它(在这种情况下,错误会永远丢失),要么继续将错误向上传递给调用它的函数。这可能会很混乱,并导致上面提到的许多相同问题。

总之,返回码的主要问题是错误处理代码最终会与正常控制流紧密缠绕在一起。这反过来会限制代码布局,也会限制我们合理处理错误的方式。


异常机制

异常处理提供了一种机制,可以将错误或其他异常情况的处理与代码的正常控制流解耦。这让错误处理更加灵活,也减轻了返回码造成的大多数混乱。

在下一课中,我们将了解异常在C++中的工作方式。


26.6 第26章总结

上一节

27.1 基本异常处理

下一节