终止(提前退出程序)

本章最后要讨论的控制流类别是“终止”(halt)。终止语句用于结束整个程序。在 C++ 中,终止功能以函数形式提供(而非关键字),因此调用终止语句实际上是调用相关函数。

先简要回顾程序正常退出的流程。当 main() 函数返回(无论是执行到函数末尾还是显式 return)时,会发生以下步骤:

  1. 离开函数作用域,所有局部变量及函数参数按常规被销毁;
  2. 随后调用特殊函数 std::exit(),并将 main() 的返回值(即状态码)作为参数传入。

std::exit() 函数

std::exit() 使程序以“正常终止”方式结束。所谓“正常终止”仅表示程序按预期退出,并不暗示程序是否成功(成功与否由状态码指示)。例如,若程序要求用户输入文件名而用户输入无效,程序可返回非零状态码表示失败,但仍属于正常终止。

std::exit() 会执行以下清理步骤:

  • 销毁所有具有静态存储期的对象;
  • 若程序曾打开文件,则执行其他杂项文件清理;
  • 最后将控制权交还操作系统,并以传给 std::exit() 的参数作为状态码。

显式调用 std::exit()

除在 main() 返回时隐式调用外,也可在程序任意位置显式调用 std::exit() 以提前终止程序。此时需包含头文件

关键洞察
当 main() 返回时,std::exit 会被隐式调用。

示例:

#include <cstdlib> // for std::exit()
#include <iostream>

void cleanup()
{
    // 在此处执行所有必要清理
    std::cout << "cleanup!\n";
}

int main()
{
    std::cout << 1 << '\n';
    cleanup();
    std::exit(0); // 终止程序并向操作系统返回状态码 0

    // 以下语句永不会执行
    std::cout << 2 << '\n';
    return 0;
}

程序输出:

1
cleanup!

注意 std::exit() 之后的语句不会执行。
此外,std::exit() 可在任意函数中调用,以在该点终止整个程序。

std::exit() 不会清理局部变量

显式调用 std::exit() 时须注意:它不会销毁当前函数及调用栈上任何局部变量。若程序依赖局部变量的析构清理行为,则调用 std::exit() 可能带来风险。

警告
std::exit() 不会清理当前函数或调用栈中的局部变量。

std::atexit
由于 std::exit() 立即终止程序,有时需要在终止前手动执行清理(如关闭数据库或网络连接、释放内存、写日志等)。

补充说明
现代操作系统在应用退出时通常会回收未释放的内存,于是有人疑问“为何还要做清理?”至少有两条理由:

  1. 主动释放内存是避免运行期内存泄漏的良好习惯;否则只在部分路径释放会导致不一致并可能引发错误。此外,内存分析工具可能无法区分“故意未释放”与“疏忽未释放”。
  2. 还有其他清理需求:如数据尚未刷盘即异常退出,可能导致数据丢失;提前关闭文件可确保缓存写入;又或需在程序结束前向服务器发送会话信息等。

前述示例中,我们手动调用 cleanup()。然而,每次调用 std::exit() 前都记得手动清理既繁琐又易出错。为此,C++ 提供 std::atexit(),允许注册函数,使其在通过 std::exit() 终止时自动调用。

相关内容
函数作为实参的讨论见函数指针。

示例:

#include <cstdlib> // for std::exit()
#include <iostream>

void cleanup()
{
    std::cout << "cleanup!\n";
}

int main()
{
    // 注册 cleanup,使其在 std::exit() 时自动调用
    std::atexit(cleanup); // 注意使用 cleanup 而非 cleanup(),因为此处并非立即调用

    std::cout << 1 << '\n';
    std::exit(0); // 终止并返回 0

    // 以下语句永不会执行
    std::cout << 2 << '\n';
    return 0;
}

输出与前例相同:

1
cleanup!

std::atexit() 的优势:只需在程序某处(通常在 main() 内)注册一次,之后便无需在每次 std::exit() 前手动调用。
注意事项:

  • 因 main() 返回时会隐式调用 std::exit(),故该方式注册的函数亦会被调用;
  • 被注册函数必须无参数且无返回值;
  • 可多次调用 std::atexit() 注册多个函数,它们按注册逆序被调用(最后注册的最先执行)。

高级提示
多线程程序中,调用 std::exit() 可能导致崩溃,因为调用线程会清理仍被其他线程访问的静态对象。因此 C++ 还提供了 std::quick_exit() 与 std::at_quick_exit(),其行为与 std::exit()/std::atexit() 类似,但 std::quick_exit() 不销毁静态对象,清理范围更小,适用于多线程场景。

std::abort 与 std::terminate

C++ 还提供另外两个终止相关函数。

std::abort() 使程序“异常终止”——通常由于运行时错误无法继续。例如除以零将导致异常终止。std::abort() 不进行任何清理。

示例:

#include <cstdlib> // for std::abort()
#include <iostream>

int main()
{
    std::cout << 1 << '\n';
    std::abort();

    // 以下语句永不会执行
    std::cout << 2 << '\n';
    return 0;
}

后续课程assert 与 static_assert 中我们将看到 std::abort 被隐式调用的情形。

std::terminate() 通常与异常配合使用(异常将在后续章节讨论)。虽可显式调用,但更多情况下是在异常未捕获(以及其他异常相关情形)时由系统隐式调用。缺省行为是调用 std::abort()。

何时应使用终止?

简短回答:“几乎从不”。销毁局部对象是 C++ 的重要机制(尤其在涉及类的场景中),而前述所有函数均不会清理局部变量。异常是更安全、更合理的错误处理手段。

最佳实践
仅当无法以正常方式从 main 函数返回时才使用终止。若未禁用异常,应优先使用异常安全地处理错误。

提示
尽管应尽量避免显式终止,程序仍可能以其他方式意外结束,例如:

  • 由于 bug 崩溃(操作系统强制终止);
  • 用户通过多种方式强制结束程序;
  • 断电或硬件故障;
  • 太阳爆发成超新星吞噬地球……

良好设计的程序应能在任意时刻被终止而后果最小化。常见示例:现代游戏会定期自动保存状态与设置,若异常退出,用户可从上次存档继续,损失极小。

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

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

公众号二维码

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