本章最后要讨论的控制流类别是“终止”(halt)。终止语句用于结束整个程序。在 C++ 中,终止功能以函数形式提供(而非关键字),因此调用终止语句实际上是调用相关函数。
先简要回顾程序正常退出的流程。当 main() 函数返回(无论是执行到函数末尾还是显式 return)时,会发生以下步骤:
- 离开函数作用域,所有局部变量及函数参数按常规被销毁;
- 随后调用特殊函数 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() 立即终止程序,有时需要在终止前手动执行清理(如关闭数据库或网络连接、释放内存、写日志等)。
补充说明
现代操作系统在应用退出时通常会回收未释放的内存,于是有人疑问“为何还要做清理?”至少有两条理由:
- 主动释放内存是避免运行期内存泄漏的良好习惯;否则只在部分路径释放会导致不一致并可能引发错误。此外,内存分析工具可能无法区分“故意未释放”与“疏忽未释放”。
- 还有其他清理需求:如数据尚未刷盘即异常退出,可能导致数据丢失;提前关闭文件可确保缓存写入;又或需在程序结束前向服务器发送会话信息等。
前述示例中,我们手动调用 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 崩溃(操作系统强制终止);
- 用户通过多种方式强制结束程序;
- 断电或硬件故障;
- 太阳爆发成超新星吞噬地球……
良好设计的程序应能在任意时刻被终止而后果最小化。常见示例:现代游戏会定期自动保存状态与设置,若异常退出,用户可从上次存档继续,损失极小。