有几种方法,但首先您需要了解为什么对象清理很重要,以及 std::exit 在 C++ 程序员中被边缘化的原因。
RAII 和堆栈展开
C++ 使用了一个称为RAII 的惯用语,简单来说,这意味着对象应该在构造函数中执行初始化并在析构函数中执行清理。例如std::ofstream 类 [可以] 在构造函数期间打开文件,然后用户对其执行输出操作,最后在其生命周期结束时,通常由其范围决定,调用析构函数,本质上关闭文件并将所有写入的内容刷新到磁盘中。
如果你没有使用析构函数来刷新和关闭文件会发生什么? 谁知道呢!但它可能不会将它应该写入的所有数据写入文件。
例如考虑这段代码
#include <fstream>
#include <exception>
#include <memory>
void inner_mad()
{
throw std::exception();
}
void mad()
{
auto ptr = std::make_unique<int>();
inner_mad();
}
int main()
{
std::ofstream os("file.txt");
os << "Content!!!";
int possibility = /* either 1, 2, 3 or 4 */;
if(possibility == 1)
return 0;
else if(possibility == 2)
throw std::exception();
else if(possibility == 3)
mad();
else if(possibility == 4)
exit(0);
}
每种可能性发生的情况是:
-
可能性1: Return 实质上离开了当前函数范围,因此它知道
os 生命周期的结束,从而调用其析构函数并通过关闭文件并将文件刷新到磁盘来进行适当的清理。
-
可能性 2: 抛出异常也会处理当前范围内对象的生命周期,从而进行适当的清理...
-
可能性 3: 这里堆栈展开开始了!即使在
inner_mad 处抛出异常,展开器也会通过mad 和main 的堆栈进行适当的清理,所有对象都将被正确销毁,包括ptr 和os。
-
可能性 4: 嗯,在这里?
exit 是一个 C 函数,它不知道也不兼容 C++ 习语。它不会对您的对象执行清理,包括同一范围内的 os。因此您的文件将无法正确关闭,因此内容可能永远不会被写入其中!
-
其他可能性:它会离开主范围,通过执行隐式
return 0 并因此具有与可能性 1 相同的效果,即适当的清理。
但是不要对我刚才告诉你的那么肯定(主要是可能性 2 和 3);继续阅读,我们将了解如何执行适当的基于异常的清理。
结束的可能方式
从主返回!
您应该尽可能地这样做;总是喜欢通过从 main 返回正确的退出状态来从您的程序返回。
您的程序的调用者,可能还有操作系统,可能想知道您的程序应该做的事情是否成功完成。出于同样的原因,您应该返回零或EXIT_SUCCESS 表示程序成功终止,EXIT_FAILURE 表示程序未成功终止,任何其他形式的返回值都是实现定义的(§18.5/8)。
但是你可能在调用堆栈中很深,并且返回所有它可能会很痛苦......
[不要]抛出异常
抛出异常将使用堆栈展开执行适当的对象清理,方法是调用任何先前范围内的每个对象的析构函数。
但这是关键!当抛出的异常未被处理(通过 catch(...) 子句) 或者即使您在调用中间有一个 noexcept 函数时,是否执行堆栈展开由实现定义堆。这在 §15.5.1 [except.terminate] 中有说明:
-
在某些情况下,必须放弃异常处理以使用不太微妙的错误处理技术。 [注意:这些情况是:
[...]
— 当异常处理机制无法为抛出的异常 (15.3) 找到处理程序时,或者当搜索处理程序 (15.3) 遇到具有 noexcept-specification 的函数的最外层时 不允许异常 (15.4),或 [...]
[...]
在这种情况下,调用 std::terminate() (18.8.3)。在没有找到匹配处理程序的情况下,在调用 std::terminate() 之前堆栈是否展开由实现定义 [...]
所以我们必须抓住它!
抛出异常并在 main 中捕获它!
由于未捕获的异常可能不会执行堆栈展开(因此不会执行适当的清理),我们应该在 main 中捕获异常,然后返回退出状态(EXIT_SUCCESS 或 EXIT_FAILURE )。
所以一个可能不错的设置是:
int main()
{
/* ... */
try
{
// Insert code that will return by throwing a exception.
}
catch(const std::exception&) // Consider using a custom exception type for intentional
{ // throws. A good idea might be a `return_exception`.
return EXIT_FAILURE;
}
/* ... */
}
[不要] std::exit
这不会执行任何类型的堆栈展开,并且堆栈上的任何活动对象都不会调用其各自的析构函数来执行清理。
这在 §3.6.1/4 [basic.start.init] 中强制执行:
在不离开当前块的情况下终止程序(例如,通过调用函数 std::exit(int) (18.5))不会破坏任何具有自动存储持续时间的对象 (12.4)。如果在销毁具有静态或线程存储持续时间的对象期间调用 std::exit 以结束程序,则该程序具有未定义的行为。
现在想想,你为什么会做这样的事情?你痛苦地损坏了多少物体?
其他 [as bad] 替代品
还有其他方法可以终止程序(除了崩溃),但不推荐使用它们。只是为了清楚起见,他们将在这里介绍。请注意正常程序终止 不意味着堆栈展开,而是操作系统的正常状态。