C++基础异常处理:throw、try与catch详解

在上一节“异常的必要性”中,我们讨论了使用返回码会导致控制流与错误流交织,从而对两者形成制约。C++ 通过三个相互配合的关键字实现异常机制:throwtrycatch

抛出异常

现实生活中,我们经常用“信号”表示特定事件的发生。例如美式足球中,若球员犯规,裁判会掷出黄旗并吹停比赛,随后判罚并执行;罚则完成后比赛照常继续。

在 C++ 中,throw 语句用于发出异常或错误信号(可类比掷出黄旗)。发出异常亦常称为“引发异常”。

使用 throw 语句时,仅需在关键字 throw 后给出任意数据类型的值,用以指示发生了错误。该值通常为错误码、问题描述或自定义异常类。

示例如下:

throw -1;                                      // 抛出整型字面量
throw ENUM_INVALID_INDEX;                      // 抛出枚举值
throw "Can not take square root of negative number"; // 抛出 C 风格字符串
throw dX;                                      // 抛出先前定义的 double 变量
throw MyException("Fatal Error");              // 抛出 MyException 类对象

以上各语句均表示某种需要处理的问题已经发生。

捕获异常

仅抛出异常并不完整。回到美式足球的比喻:裁判掷出黄旗后,接下来会怎样?球员注意到犯规并停止比赛,比赛的正常流程被打断。

在 C++ 中,我们使用关键字 try 定义一段语句序列(称为 try 块)。try 块充当观察者,监视该块内任何语句抛出的异常。

示例:

try
{
    // 可能抛出欲处理异常的语句置于此处
    throw -1; // 一条简单的 throw 语句
}

注意,try 块并不定义如何处理异常;它仅告诉程序:“若本块内有语句抛出异常,请将其捕获!”

处理异常

美式足球比喻的最后一步:裁判判罚并执行,比赛方能继续。在 C++ 中,异常的实际处理由 catch 块完成。catch 关键字用于定义一段代码(catch 块),该块负责处理某一特定数据类型的异常。

示例:捕获整型异常的 catch 块

catch (int x)
{
    // 在此处处理 int 类型异常
    std::cerr << "捕获到 int 异常,其值为:" << x << '\n';
}

try 块与 catch 块协同工作:try 块检测其内部抛出的异常,并将其路由至类型匹配的 catch 块处理。一个 try 块必须至少紧跟一个 catch 块,且可连续列出多个 catch 块。

异常一旦被 try 块捕获并交由匹配的 catch 块处理后,即视为已处理。catch 块执行完毕后,程序从最后一个 catch 块之后的语句继续正常执行。

catch 参数与函数参数规则相同,参数在随后的 catch 块中可用。对于基本类型异常,可按值捕获;对于非基本类型异常,应按 const 引用捕获,以避免不必要的复制,并在某些情况下防止对象切片。

若 catch 块内不使用该参数,可省略变量名:

catch (double) // 注意:因 catch 块内不使用,故无变量名
{
    // 处理 double 类型异常
    std::cerr << "捕获到 double 类型异常\n";
}

如此可避免编译器关于未使用变量的警告。

异常匹配时不会进行类型转换(int 异常不会匹配 double 参数的 catch 块)。

综合示例:throw、try 与 catch

以下完整程序演示了 throw、try 及多个 catch 块的用法:

#include <iostream>
#include <string>

int main()
{
    try
    {
        // 可能抛出欲处理异常的语句置于此处
        throw -1; // 简单示例
    }
    catch (double) // 不使用异常对象,故无变量名
    {
        // 上述 try 块内抛出的 double 类型异常将被路由至此
        std::cerr << "捕获到 double 类型异常\n";
    }
    catch (int x)
    {
        // 上述 try 块内抛出的 int 类型异常将被路由至此
        std::cerr << "捕获到 int 异常,其值为:" << x << '\n';
    }
    catch (const std::string&) // 以 const 引用捕获类类型
    {
        // 上述 try 块内抛出的 std::string 类型异常将被路由至此
        std::cerr << "捕获到 std::string 类型异常\n";
    }

    // 异常处理完毕后,程序从 catch 块后继续执行
    std::cout << "继续愉快地前行\n";

    return 0;
}

在作者机器上,上述 try/catch 组合输出如下:

捕获到 int 异常,其值为 -1
继续愉快地前行

throw 语句引发类型为 int、值为 -1 的异常,该异常被包裹它的 try 块捕获,并路由至处理 int 的 catch 块,后者输出了相应错误信息。异常处理完毕后,程序继续正常执行。

异常处理回顾

异常处理其实十分简洁,下面两段话几乎涵盖了你需要牢记的全部要点:

当异常被引发(使用 throw)时,运行中的程序会寻找最近的包围 try 块(必要时沿调用栈向上查找,后续将详述),并检查附加于该 try 块的 catch 处理器能否处理该异常类型。若匹配成功,执行跳转到对应 catch 块的顶端,异常即视为已处理。

若最近的包围 try 块中没有合适的 catch 处理器,程序继续向外层 try 块搜寻。若直至程序结束仍未找到合适处理器,程序将以运行时异常错误终止。

注意:程序在匹配异常与 catch 块时不会进行隐式类型转换或提升!例如,char 异常不会匹配 int catch 块;int 异常也不会匹配 float catch 块。然而,派生类向其基类的转换会被执行。

至此,你已掌握异常处理的核心内容。本章其余部分将通过示例进一步展示这些原则的应用。

异常会被立即处理

以下小程序演示异常被立即处理的特性:

#include <iostream>

int main()
{
    try
    {
        throw 4.5; // 抛出 double 类型异常
        std::cout << "此行永远不会打印\n";
    }
    catch (double x) // 处理 double 类型异常
    {
        std::cerr << "捕获到 double,其值为:" << x << '\n';
    }

    return 0;
}

程序极简,其执行流程如下:throw 语句首先执行,引发 double 类型异常。执行立即跳转到最近的包围 try 块(本程序唯一 try 块),随后检查 catch 处理器。异常类型为 double,于是匹配成功,执行对应 catch 块。

程序输出:

捕获到 double,其值为:4.5

注意,“This never prints” 永远不会出现,因为异常使执行路径立即跳转到异常处理器。

更具现实意义的示例

以下示例不再那么“学术”:

#include <cmath>   // 引入 sqrt() 函数
#include <iostream>

int main()
{
    std::cout << "请输入一个数:";
    double x {};
    std::cin >> x;

    try // 监控 try 块内异常并路由至附属 catch 块
    {
        // 若用户输入负数,则为错误情况
        if (x < 0.0)
            throw "Can not take sqrt of negative number"; // 抛出 const char* 类型异常

        // 否则,输出结果
        std::cout << x << " 的平方根为 " << std::sqrt(x) << '\n';
    }
    catch (const char* exception) // 捕获 const char* 类型异常
    {
        std::cerr << "错误:" << exception << '\n';
    }
}

在此代码中,程序提示用户输入一个数。若输入正数,条件语句不成立,无异常抛出,程序直接输出平方根;此时 catch 块不会执行。示例如下:

请输入一个数:9
9 的平方根为 3

若用户输入负数,则抛出 const char* 类型异常。由于处于 try 作用域且找到匹配处理器,控制立即转移到 const char* 异常处理器,输出:

请输入一个数:-4
错误:Can not take sqrt of negative number

至此,你已掌握异常的基本思想。接下来将通过更多示例展示异常的灵活性。

catch 块通常做什么

若异常被路由到 catch 块,即使该块为空,亦视为“已处理”。然而,通常你会希望 catch 块完成有用操作。一般而言,catch 块在捕获异常后有四种常见行为:

  1. 打印错误信息(输出至控制台或日志文件),随后允许函数继续执行;
  2. 向调用者返回一个值或错误码;
  3. 抛出新异常。由于 catch 块位于 try 块之外,此时抛出的新异常不会被之前的 try 块捕获,而由更外层 try 块处理;
  4. main() 中的 catch 块可捕获致命错误,并优雅终止程序。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"