(在“提示:此答案已从 Does a memory leak cause undefined behaviour? 移至此处”下方评论 - 您可能必须阅读该问题才能获得此答案的正确背景 O_o)。
在我看来,这部分标准明确允许:
我无法理解为什么当存在对副作用的依赖时,标准选择不定义行为 - 而不是简单地说这些副作用不会有发生了,让程序定义或未定义的行为,就像你通常期望的那样。
我们可以仍然考虑什么标准所说的未定义行为。关键部分是:
“取决于析构函数产生的副作用有未定义的行为。”
标准 §1.9/12 明确定义副作用如下(以下斜体为标准,表示引入了正式定义):
访问由volatile glvalue (3.10) 指定的对象、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数都是副作用 ,即执行环境状态的变化。
在您的程序中,没有依赖关系,因此没有未定义的行为。
一个可以说与§3.8 p4 中的场景相匹配的依赖示例是:
struct X
{
~X() { std::cout << "bye!\n"; }
};
int main()
{
new X();
}
人们正在争论的一个问题是上面的 X 对象是否会被视为 3.8 p4 的目的 released,因为它可能只发布到操作系统。程序终止后 - 从阅读标准中不清楚流程“生命周期”的那个阶段是否在标准的行为要求范围内(我对标准的快速搜索没有澄清这一点)。我个人认为 3.8p4 在这里适用,部分原因是只要它足够模棱两可,编译器编写者可能会觉得有权在这种情况下允许未定义的行为,但即使上面的代码不构成释放场景的容易修改啦...
int main()
{
X* p = new X();
*(char*)p = 'x'; // token memory reuse...
}
无论如何,尽管 main 实现了上面的析构函数有一个副作用 - 每个“调用库 I/O 函数”;此外,程序的可观察行为可以说是“依赖于”它,因为如果它已经运行,会受到析构函数影响的缓冲区在终止期间被刷新。但是“取决于副作用”仅是否意味着暗示如果析构函数没有运行,程序显然会有未定义的行为的情况?我会在前者方面犯错,特别是因为后一种情况不需要标准中的专用段落来记录行为未定义。这是一个明显未定义行为的示例:
int* p_;
struct X
{
~X() { if (b_) p_ = 0; else delete p_; }
bool b_;
};
X x{true};
int main()
{
p_ = new int();
delete p_; // p_ now holds freed pointer
new (&x){false}; // reuse x without calling destructor
}
在终止期间调用x 的析构函数时,b_ 将是false,因此~X() 将delete p_ 用于已释放的指针,从而创建未定义的行为。如果在重用之前调用了x.~X();,则p_ 将被设置为0,并且删除将是安全的。从这个意义上说,程序的正确行为可以说取决于析构函数,并且行为显然是未定义的,但是我们是否只是制作了一个与 3.8p4 描述的行为本身匹配的程序,而不是让行为成为结果3.8p4...?
有问题的更复杂的场景 - 太长而无法提供代码 - 可能包括例如一个奇怪的 C++ 库,在文件流对象中具有引用计数器,必须达到 0 才能触发某些处理,例如刷新 I/O 或加入后台线程等 - 不做这些事情的风险不仅在于无法执行明确请求的输出析构函数,但也无法从流中输出其他缓冲输出,或者在某些具有事务文件系统的操作系统上可能会导致早期 I/O 的回滚 - 此类问题可能会改变可观察到的程序行为甚至使程序挂起。
注意:没有必要证明任何实际代码在任何现有编译器/系统上行为异常;标准清楚地保留了编译器具有未定义行为的权利......这就是最重要的。这不是您可以推理并选择忽略标准的事情 - 可能是 C++14 或其他一些修订版更改了此规定,但只要它存在,那么即使可以说对 有一些“依赖”副作用那么就有可能出现未定义的行为(当然,它本身允许由特定的编译器/实现来定义,所以这并不意味着每个编译器都必须做一些奇怪的事情)。