C++异常处理:函数与栈回卷深度解析

在上一节《基础异常处理》中,我们说明了 throw、try 与 catch 如何协同工作以实现异常处理。本节将讨论异常处理与函数之间的交互关系。

在被调函数中抛出异常

上一节曾指出:“try 块会检测其内部语句抛出的任何异常”。相应示例中,throw 语句与对应的 catch 块均位于同一函数内,这种做法价值有限。

更值得关注的是:若 try 块内的语句是一次函数调用,而被调函数抛出了异常,try 块是否能检测到?答案是肯定的!

异常处理最具实用性的特性之一在于:throw 语句不必直接位于 try 块内。异常可在函数任意位置抛出,并由调用者的 try 块(或更外层调用者)捕获。当异常以这种方式被捕获时,执行流将从抛出点直接跳转到处理该异常的 catch 块。

关键洞察

try 块不仅能捕获其内部语句抛出的异常,还能捕获这些语句所调用函数抛出的异常。

这使异常处理更具模块化。我们将上一节的平方根程序重构为模块化函数以说明这一点:

#include <cmath>   // sqrt()
#include <iostream>

// 模块化的平方根函数
double mySqrt(double x)
{
    if (x < 0.0)                          // 负数视为错误
        throw "Can not take sqrt of negative number"; // 抛出 const char* 异常

    return std::sqrt(x);
}

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

    try                                      // 监控 try 块内异常
    {
        double d = mySqrt(x);
        std::cout << "The sqrt of " << x << " is " << d << '\n';
    }
    catch (const char* exception)            // 捕获 const char* 异常
    {
        std::cerr << "Error: " << exception << std::endl;
    }

    return 0;
}

程序运行结果与预期一致:

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

当 mySqrt() 抛出异常时,该函数内部并无处理器。但 main() 中对 mySqrt() 的调用位于 try 块内,且存在匹配处理器,于是执行从 mySqrt() 内的 throw 语句直接跳转到 main() 的 catch 块,随后继续执行。

最值得注意的是:mySqrt() 可以抛出异常,却不自行处理,相当于声明“出现问题!”却把处理责任交给调用者(类似用返回码将错误处理责任回传)。

为何不把错误处理留在 mySqrt() 内?不同应用对错误处理的需求各异:控制台程序可能打印文本;窗口程序可能弹出对话框;某场景下该错误致命,另一场景则未必。通过将错误传递出去,各应用可按最合适的方式处理,保持 mySqrt() 最大化模块化,而把错误处理放在更具体的上下文中。

异常处理与栈回卷

本部分阐述多函数场景下异常处理的实际流程。

相关内容

如需回顾调用栈与栈回卷,请参见《栈与堆》。

当异常被抛出时,程序首先检查当前函数能否立即处理(即异常在当前函数的 try 块内抛出且存在对应 catch)。若能,则就地处理。

若不能,程序继续检查调用者(调用栈上一层)能否处理。要让调用者处理,当前函数的调用必须位于调用者的 try 块内,且存在匹配的 catch。若仍未找到,则检查再上一层,依此类推。

该检查沿调用栈逐层向上,直至找到处理器或查遍所有函数仍未果。

若找到匹配的异常处理器,执行从抛出点跳转到对应 catch 块顶部。这需要回卷栈(将当前函数及中间函数逐层弹出调用栈),直至处理异常的函数位于栈顶。

若未找到匹配处理器,栈可能回卷也可能不回卷,下一节《未捕获异常与通配处理器》将详细讨论。

当函数被弹出调用栈时,其所有局部变量照常销毁,但不会返回值

关键洞察

栈回卷会销毁被弹出函数的局部变量(确保析构函数执行,这是好事)。

又一个栈回卷示例

为说明上述过程,我们看一个更复杂的例子,涉及更深的调用栈。程序虽长但逻辑简单:main() → A() → B() → C() → D(),D() 抛出异常。

#include <iostream>

void D() // 被 C() 调用
{
    std::cout << "Start D\n";
    std::cout << "D throwing int exception\n";

    throw -1;

    std::cout << "End D\n"; // 不会执行
}

void C() // 被 B() 调用
{
    std::cout << "Start C\n";
    D();
    std::cout << "End C\n";
}

void B() // 被 A() 调用
{
    std::cout << "Start B\n";

    try
    {
        C();
    }
    catch (double) // 不匹配:异常类型不符
    {
        std::cerr << "B caught double exception\n";
    }

    try
    {
    }
    catch (int) // 不匹配:对 C() 的调用不在此 try 块内
    {
        std::cerr << "B caught int exception\n";
    }

    std::cout << "End B\n";
}

void A() // 被 main() 调用
{
    std::cout << "Start A\n";

    try
    {
        B();
    }
    catch (int) // 匹配成功并处理
    {
        std::cerr << "A caught int exception\n";
    }
    catch (double) // 已在前一 catch 处理,不会执行
    {
        std::cerr << "A caught double exception\n";
    }

    // 异常处理后从此处继续
    std::cout << "End A\n";
}

int main()
{
    std::cout << "Start main\n";

    try
    {
        A();
    }
    catch (int) // 已由 A 处理,不会执行
    {
        std::cerr << "main caught int exception\n";
    }
    std::cout << "End main\n";

    return 0;
}

运行结果如下:

Start main
Start A
Start B
Start C
Start D
D throwing int exception
A caught int exception
End A
End main

流程解析

所有 “Start” 输出按部就班。D() 打印 “D throwing int exception” 后抛出 int 异常,故事由此展开。

D() 不处理异常,于是沿调用栈向上检查。C() 无任何异常处理器。
B() 有两个 try 块:其一含对 C() 的调用,但仅捕获 double,类型不匹配;另一 try 块虽能捕获 int,但对 C() 的调用不在其内,故不匹配。
A() 的 try 块包含对 B() 的调用,且存在 int 处理器,于是 A() 处理异常并打印 “A caught int exception”。

异常处理后,控制流在 A() 的 catch 块后继续,打印 “End A”,随后正常返回 main()。由于异常已由 A() 处理,main() 的 catch 块不会执行,main() 仅打印 “End main” 并正常结束。

程序展示了若干重要原则:

  1. 抛出异常的函数的直接调用者并非必须处理异常;C() 把责任转交给更上层。
  2. 若 try 块不含匹配的 catch,则等同于无 try 块,同样发生栈回卷;B() 即属此类。
  3. 若函数虽有匹配 catch,但对当前函数的调用不在对应 try 块内,该 catch 不会使用;B() 的第二个 try 块即如此。
  4. 一旦匹配 catch 执行完毕,执行流从该 catch 块后继续;A() 处理异常后继续打印 “End A”。回到 main() 时异常早已处理完毕,main() 甚至不知道异常曾发生!

可见,栈回卷带来极大便利:若函数不愿处理异常,可置之不理,异常将沿栈上浮,直至找到愿意处理的函数。这使我们得以在调用栈中最合适的位置处理错误。

下一节,我们将探讨异常未被捕获的情形,并给出预防方法。

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

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

公众号二维码

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