• Index

捕获异常

Reads: 37

异常的捕获

当遇到异常时,如果你能在当前函数处理这个错误,就应该捕获然后处理错误;如果不能在当前函数处理错误就应该让它继续抛出异常,一直到能处理这个错误的地方,再捕获处理错误;如果一直都不能处理,就让它直接终止程序。这是处理异常错误的守则。

基础示例

接下来就是捕获异常然后处理的例子:从终端输入两个数,然后求这两个数的和,如果输入的不是数则要求重新输入。

#include <iostream> // std::cin std::cout
#include <string> // std::stoi std::string
#include <stdexcept> // std::invalid_argument

// 从控制台读入一个数字
long long read(void);

int main(void)
{
    auto total = read() + read();
    std::cout << total << std::endl;
    return 0;
}

long long read(void)
{
    long long number = 0;
    for (;;)
    {
        std::string text;
        std::cin >> text;
        try
        {
            number = std::stoll(text);
            break;
        }
        catch (const std::invalid_argument &e)
        {
            std::cout << e.what() << std::endl;
            std::cout << "请输入一数字" << std::endl;
        }
    }
    return number;
}

基础讲解

如代码中所见,捕获异常使用以下语句:

try
{
}
catch (异常对象)
{
}

我们参看std::stoll说明文档,知道它有两个异常:当不能转换的时候(即非数字的时候),将会抛出std::invalid_argument类型的异常对象;当输入的数超出long long的范围时,抛出std::out_of_range类型的异常对象。再看看题目输入两个数,如果输入的不是数则要求重新输入,那么当不是数的时候提示用户不是数应该重新输入,这个做法是合理的;而用户输入很大的数,虽然很大但是它符合题目,这个错误不应该自己在代码中解决,而应该问出题的人怎样解决,那么在问之前都不应该捕获错误,就算捕获错误,也只是记录一下错误,因为你也解决不了比long long还大的数怎么保存和计算,对吧?

所以示例代码中,只捕获std::invalid_argument类型的异常,其他异常由于没有捕获将继续抛出异常,也就是下面的局部代码。

try
{
    number = std::stoll(text);
    break;
}
catch (const std::invalid_argument &e)
{
    std::cout << e.what() << std::endl;
    std::cout << "请输入一数字" << std::endl;
}

我们知道,当异常出现时,将不再继续执行剩下的代码并且抛出异常。上面的这个局部代码,long long number = std::stoll(text);这个语句抛出异常后,return number;将不会执行。由于它在捕获语句中抛出异常,所以当它抛出异常时,就会进行判断,当它抛出的异常是std::invalid_argument时,就会被捕获而去执行catch里面的代码;当它抛出的异常不是std::invalid_argument时,由于没有写出捕获语句,所以还是会抛出异常。而类std::invalid_argumentstdexcept标准库中,所以需要引入该库。而catch中使用的是引用,防止复制对象造成额外损耗。

标准库中所有异常类都继承自类std::exception,它有虚析构函数,并且有一个虚函数叫做what,用于输出异常的错误信息。由于std::invalid_argumentstd::exception的派生类,所以对象引用e也能调用成员函数what来获取异常错误信息,返回的是保存异常错误信息的字符串。其中类std::exceptionexception标准库中。事实上,按照题意,这里不需要输出输出异常信息,输出异常错误信息只是为了讲解如何输出异常错误信息。

当使用catch捕获了异常后,就相当于要处理异常,那么当catch执行完毕后,他会继续执行捕获语句后的代码,而捕获语句后面的代码就是返回到for循环的开头。因此,这部分代码的逻辑就是当输入的不是数值时,就会一直循环,直到输入数值。

再看回try语句,当它顺利转换数值后,就会继续执行下一行代码,也就是break;。顺利执行不抛出异常的代码就相当于:

long long read(void)
{
    long long number = 0;
    for (;;)
    {
        std::string text;
        std::cin >> text;
        number = std::stoll(text);
        break;
    }
    return number;
}

那么它顺利转换成数值后,就会遇到break,然后退出循环,退出循环后的语句就是返回转换后的数值。因此整个函数的逻辑就是:当输入的不是数值时,就会一直循环,直到输入数值,然后退出循环并返回转换后的数值。

或许你会问,为什么break;要放在try里面?如果break;放在捕获语句之后,那么catch处理之后就会遇上break;退出循环;放在try里面,当std::stoll顺利执行就可以继续执行break;,当std::stoll抛出异常时,break;就不会执行。这是符合逻辑的。

上面写的读取函数的代码是为了讲解异常方便而写的,事实上,上面的函数可以简化为下面这样:

long long read(void)
{
    for (;;)
    {
        std::string text;
        std::cin >> text;
        try
        {
            return std::stoll(text);
        }
        catch (const std::invalid_argument &)
        {
            std::cout << "请输入一数字" << std::endl;
        }
    }
}

上面的代码,先执行std::stoll(text);,当转换失败时抛出异常;转换成功就会返回转换后的数值,然后再返回这个数值。由于需要捕获std::invalid_argument但不需要操作错误信息,所以就写成const std::invalid_argument &。简化后的代码逻辑是:当输入的不是数值时,就会输出提示然后继续循环,直到输入数值,然后直接返回转换后的数值。

不过,以上说的都是事先知道错误的,而也有大多数情况是不知道会出现什么错误,当出现这种情况时,如果你没有更好的办法,就只能在主函数处捕获异常,记录错误信息,然后退出程序或者重启程序。

基础拓展

继续说捕获语句。

如果你对所有错误都有不同的处理方法,那么可以像下面这样,逐个异常列出来然后处理:

try
{
    std::stoll("error");
}
catch (const std::invalid_argument &)
{
    // 处理无效参数的错误
}
catch (const std::out_of_range &)
{
    // 处理超出范围的错误
}

如果你只对部分的错误有处理方法,剩下的可以统一处理。例如下面代码只处理无效参数的错误,其他的错误都会捕获,并且都统一捕获到const std::exception &这里来处理:

try
{
    std::stoll("error");
}
catch (const std::invalid_argument &)
{
    // 处理无效参数的错误
}
catch (const std::exception &)
{
    // 统一处理其他错误
}

如果你对所有的异常都统一处理。那么可以这样写:

try
{
    std::stoll("error");
}
catch (const std::exception &)
{
    // 统一处理错误
}

比较特殊的是,如果你不需要错误信息,那么上面代码可以进一步简写为:

try
{
    std::stoll("error");
}
catch (...)
{
    // 统一处理错误
}

而且,捕获语句不止在函数里的代码上使用,也可以直接在函数上使用。例如:

int main(void)
try
{
    return 0;
}
catch (...)
{
}

是不是很有趣。o( =•ω•= )m

捕获异常的顺序

基础示例

#include <iostream> // std::cout
#include <string> // std::stoi
#include <stdexcept> // std::invalid_argument

int main(void)
{
    try
    {
        std::stoll("error");
    }
    catch (const std::exception &)
    {
        std::cout << "有异常出现" << std::endl;
    }
    catch (const std::invalid_argument &)
    {
        std::cout << "你输入了无效参数" << std::endl;
    }
    return 0;
}

输出结果:

有异常出现

基础讲解

当这样写的时候,编译器有可能会报出警告,说处理所有异常的catch语句应该放到最后。我们也可以从结果看出,异常的捕获是从上至下一个个找符合条件的异常类型,当找到第一个符合异常类型的catch时,就会直接处理,而跳过剩下的捕获类型。上面代码将通用处理放在具体异常类型后面,那么后面的具体异常类型就不可能被执行到,所以编译器才会提示出警告。


Comments

Make a comment

  • Index

WARNING: You are using an old browser that does not support HTML5. Please choose a modern browser (Chrome / Microsoft Edge / Firefox / Sarafi) to get a good experience.