8-12 halt(提前退出程序)

本章要介绍的最后一种流程控制语句是终止语句。终止halt语句是一种结束程序运行的流程控制语句。在C++中,终止语句通过函数(而非关键字)实现,因此我们的终止语句将以函数调用的形式呈现。

让我们稍作偏离,回顾程序正常退出时的处理流程。当 main() 函数返回(无论是执行到函数末尾,还是通过 return 语句)时,会发生以下一系列操作:

首先,由于函数即将退出,所有局部变量和函数形参将按常规机制被销毁。

接着,系统会调用名为std::exit()的特殊函数,并将main()的返回值(状态码)作为实参传递。那么std::exit()究竟是什么?


std::exit() 函数

std::exit() 是一个使程序正常终止的函数。正常终止Normal termination意味着程序以预期方式退出。需注意,“正常终止”一词并不暗示程序是否成功(这需要状态码来体现)。例如,假设你编写的程序要求用户输入待处理的文件名。若用户输入了无效文件名,程序可能会返回非零状态码以指示失败状态,但仍属于正常终止。

std::exit() 会执行多项清理操作:首先销毁静态存储期的对象;若涉及文件操作则执行相关清理;最后将控制权交还操作系统,并将传递给 std::exit() 的实参作为状态码返回。


显式调用 std::exit()

尽管在 main() 函数返回后会隐式调用 std::exit(),但也可以显式调用 std::exit() 在程序正常终止前强制终止程序。当以这种方式调用 std::exit() 时,需要包含 cstdlib 头文件。

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

以下是显式使用 std::exit() 的示例:

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

void cleanup()
{
    // code here to do any kind of cleanup required
    std::cout << "cleanup!\n";
}

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

    std::exit(0); // terminate and return status code 0 to operating system

    // The following statements never execute
    std::cout << 2 << '\n';

    return 0;
}

该程序输出:

image

请注意,std::exit()调用之后的语句永远不会执行,因为程序已经终止。

尽管在上面的程序中我们从main()函数调用了std::exit(),但std::exit()可以从任何函数调用,以在该点终止程序。


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

关于显式调用 std::exit() 的重要说明:std::exit() 不会清理任何局部变量(无论是在当前函数中,还是在调用栈上层的函数中)。这意味着,如果您的程序依赖于局部变量自行清理,调用 std::exit() 可能会带来风险。

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


std::atexit

由于 std::exit() 会立即终止程序,您可能需要在终止前手动执行一些清理工作。在此情境下,清理工作包括关闭数据库或网络连接、释放已分配的内存、将信息写入日志文件等操作。

顺带一提……

当应用程序退出时,现代操作系统通常会清理应用程序自身未正确清理的内存。这引出了一个问题:“既然如此,退出时为何还要费心清理?”至少有两个原因:

清理已分配的内存是一种“良好习惯”,你需要在应用程序运行期间使用它来避免内存泄漏。在某些情况下清理而在其他情况下不清理是不一致的,可能导致错误。不正确清理内存还会影响某些工具(如内存分析器)的行为(它们可能无法区分你无意中未清理的内存与你故意不清理的内存——后者是因为你确实无需清理)。

还有其他类型的清理操作可能对程序的可预测行为至关重要。例如,若向文件写入数据后意外退出程序,该数据可能尚未写入文件,导致程序终止时丢失。关闭文件前先写入可确保缓存数据全部写入。或者,您可能需要在程序实际关闭前,将用户会话信息或关闭原因发送至服务器。

在上例中,我们调用了清理函数 cleanup() 来处理清理任务。然而,在每次调用 exit() 之前都手动调用清理函数,不仅增加了程序员的负担,更是错误的温床。

为解决此问题,C++ 提供了 std::atexit() 函数,它允许指定一个函数,该函数将在程序通过 std::exit() 终止时自动调用。

相关内容
我们在第20.1课——函数指针中讨论了将函数作为实参传递的内容。

以下是一个示例:

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

void cleanup()
{
    // code here to do any kind of cleanup required
    std::cout << "cleanup!\n";
}

int main()
{
    // register cleanup() to be called automatically when std::exit() is called
    std::atexit(cleanup); // note: we use cleanup rather than cleanup() since we're not making a function call to cleanup() right now

    std::cout << 1 << '\n';

    std::exit(0); // terminate and return status code 0 to operating system

    // The following statements never execute
    std::cout << 2 << '\n';

    return 0;
}

该程序的输出结果与前例相同:

image

请注意,当我们将 cleanup() 函数作为实参传递时,应使用 cleanup(函数名),而非 cleanup()(后者会实际调用该函数)。

std::atexit() 的优势在于只需调用一次(通常在 main() 函数内部)。由于 std::atexit() 会在退出时自动调用,我们在调用 std::exit() 之前无需额外调用任何清理程序。

关于 std::atexit() 与清理函数的几点说明:首先,当 main() 终止时会隐式调用 std::exit(),因此程序通过此方式退出时,所有通过 std::atexit() 注册的函数都会被调用。其次,注册的函数必须不带参数且无返回值。最后,可通过 std::atexit() 注册多个清理函数,它们将按注册顺序的逆序调用(最后注册的函数将最先执行)。

对于进阶读者

在多线程程序中,调用 std::exit() 可能导致程序崩溃(因为调用 std::exit() 的线程会清理静态对象,而这些对象可能仍被其他线程访问)。为此,C++引入了另一对与std::exit()和std::atexit()功能相似的函数:std::quick_exit()和std::at_quick_exit()。其中std::quick_exit()会正常终止程序,但不会清理静态对象,且可能执行其他类型的清理操作。std::at_quick_exit() 在程序通过 std::quick_exit() 终止时,承担与 std::atexit() 相同的作用。


std::abort 和 std::terminate

C++ 还包含另外两个与终止相关的函数。

std::abort() 函数会导致程序异常终止。异常终止Abnormal termination意味着程序在运行时遇到某种异常错误,无法继续执行。例如,尝试除以 0 就会导致异常终止。std::abort() 不会执行任何清理操作。

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

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

    // The following statements never execute
    std::cout << 2 << '\n';

    return 0;
}

在后续的第9.6节——断言与static_assert中,我们将看到std::abort被隐式调用的情况。

std::terminate()函数通常与异常机制配合使用(异常将在后续章节详述)。虽然可显式调用std::terminate,但更常见的是在异常未处理时(以及其他少数异常相关场景)隐式调用。默认情况下,std::terminate()会调用std::abort()。


何时应使用halt?

简短的回答是“几乎从不”。销毁局部对象是C++的重要组成部分(尤其涉及类时),而上述函数均不会清理局部变量。异常处理是更优且更安全的错误处理机制。

最佳实践
仅当主函数无法通过安全合理的方式正常返回时才使用halt。若未禁用异常处理,请优先采用异常机制安全处理错误。

提示

虽然应尽量减少显式使用halt指令,但程序仍可能因多种原因意外终止。例如:

  • 应用程序可能因程序错误崩溃(此时操作系统会强制关闭程序)。
  • 用户可能通过各种方式强制终止应用程序。
  • 用户可能关闭(或意外断开)计算机电源。
  • 太阳可能发生超新星爆发,将地球吞噬于巨型火球之中。

设计良好的程序应能承受任意时刻的强制关闭,且影响最小。
典型示例是现代游戏常定期自动保存游戏状态和用户设置,即使未保存时意外关闭,用户也能通过先前自动存档继续游戏,进度损失极小。

posted @ 2026-02-27 17:57  游翔  阅读(1)  评论(0)    收藏  举报