Skip to content

14.1 为什么需要异常

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

翻译by dashjay 2020.07.13

在之前的课程中,有关错误处理时,我们讨论了关于使用 assert()cerr()exit() 来处理错误的方法。那时我们提到了一个将会讲的话题,然后我们现在就要开始讲:异常

失败时返回状态码

当我们编写可复用的代码时,错误处理是一个必选项。最常见的处理错误的方式之一是通过返回状态码,例如:

int findFirstChar(const char* string, char ch)
{
    const std::size_t stringlength{ strlen(string) };

    // 逐个调试所有字符串
    for (std::size_t index=0; index < stringlength ; ++index)
        // 如果匹配返回下标对应索引...
        if (string[index] == ch)
            return index;

    // 如果没有匹配上,返回-1
    return -1;
}

这个函数返回第一个首字符匹配的字符串索引,如果字符没有被找到,函数返回 -1 用作错误指示。

这个基础的方法是相当的简单,然而,使用返回状态码有一些缺点,然而这些缺点可能在一些例子中变得尤其明显:

第一: 返回值含义模糊——如果一个函数返回 -1 ,它尝试表明一个错误,而或者那就是一个正常的返回值?如果不进入一个函数查看具体实现,通常很难知道。

第二: 函只能有一个返回值,因此如果你需要同时返回一个函数结果和一个错误码,思考下面的函数:

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

这个函数肯定需要一些错误处理,因为如果用户传入一个 0 作为变量 y,它将会崩溃。然而,它也需要返回 x/y 的结果。它如何才能兼顾?最常见的答案就是将结果或者错误处理值传入作为一个引用变量。那样的代码看起来很糟糕,也不方便使用,例如:

#include <iostream>

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

    success = 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
        cout << "The answer is " << result << '\n';
}

第三: 在一堆代码中可能有许多事情可能会报错,错误状态码会被不断的检查。

思考下面的代码片段,该片段涉及到解析一个文字文件,用来获取一些存在文件中的值:

    std::ifstream setupIni("setup.ini"); // open setup.ini for reading
                                         // 打开 并读取 setup.ini
    // If the file couldn't be opened (e.g. because it was missing) return some error enum
    // 如果不能被打开(例如:因为它丢失了),返回了一些错误枚举
    if (!setupIni)
        return ERROR_OPENING_FILE;

    // Now read a bunch of values from a file
    // 现在从文件中读出一堆值
    if (!readIntegerFromFile(setupIni, m_firstParameter)) // try to read an integer from the file
                                                          // 尝试从文件中读取一个整型
        return ERROR_READING_VALUE; // Return enum value indicating value couldn't be read
                                    // 返回一个枚举值表示值不能被读取

    if (!readDoubleFromFile(setupIni, m_secondParameter)) // try to read a double from the file
        return ERROR_READING_VALUE;

    if (!readFloatFromFile(setupIni, m_thirdParameter)) // try to read a float from the file
        return ERROR_READING_VALUE;

目前为止,我们还没有讲过文件访问,如果你不知道上面的代码时如何工作的,不要因此担心。

注意每次都调用需要一个错误检查并且返回给调用者。现在想象如果有20个不同的类型——你将检查一个错误,并且返回 ERROR_READING_VALUE 二十次!所有这些不同的错误检查和返回值使得确定函数要做的事情变得更加难以识别。(All of this error checking and returning values makes determining what the function is trying to do much harder to discern.)

第四: 返回错误码不能和构造函数很好的搭配。如果你创建一个对象时构造函数发生不可恢复的错误,将会发生什么?构造函数没有返回值传回一个状态指示,或许你可以通过传入引用来传回一个状态码,但这样会很混乱,并且必须被显式的检查。此外,即便你这样做了,对象仍然会被创建并且紧接不得不被处理( the object will still be created and then has to be dealt with or disposed of.)

最后: 当一个错误码返回给调用者,调用者可能不具有处理这个错误的能力。如果调用者不能或者不想处理这个错误,调用者可以忽略错误(在这种情况下错误将会永远丢失),或者返回出错误到调用该函数的函数。这可能很乱并且引起很多以上提到的同样的问题。

总的来说,返回状态码的基本问题就是错误码错综复杂的和代码中的常规控制流混合在一起。这些最终反过来不仅限制了代码如何编写,同时还限制了错误如何能被合理的处理。

异常

异常处理提供了一个机制,将处理错误与你代码里常规控制流中出现的异常情况解耦。这可以给我们更多的自由来决定在当前情况下,何时或者如何处理错误,减轻了许多(或许不是所有)返回错误码时的混乱。

In the next lesson, we’ll take a look at how exceptions work in C++.

在接下来的课程中,我们将看看异常在C++中是如何工作的。