sha*_*oth 14 c++ memory-leaks memory-management undefined-behavior
事实证明,许多无辜的东西都是C++中未定义的行为.例如,一旦一个非空的指针已被delete"D 甚至在打印的是指针的值是未定义的行为.
现在内存泄漏肯定是坏事.但他们是什么类的情况 - 定义,未定义或其他类别的行为?
Mar*_*ork 28
没有未定义的行为.泄漏记忆是完全合法的.
未定义的行为:标准特别不希望定义的动作,并留给实现,以便在不违反标准的情况下灵活地执行某些类型的优化.
内存管理定义明确.
如果您动态分配内存而不释放它.然后,内存仍然是应用程序的属性,以便在其认为合适时进行管理.您丢失了对该部分内存的所有引用的事实既不在这里也不在那里.
当然,如果你继续泄漏,那么你最终将耗尽可用内存,应用程序将开始抛出bad_alloc异常.但这是另一个问题.
内存泄漏在C/C++中明确定义.
如果我做:
int *a = new int[10];
Run Code Online (Sandbox Code Playgroud)
其次是
a = new int[10];
Run Code Online (Sandbox Code Playgroud)
我肯定是在泄漏内存,因为无法访问第一个分配的数组,并且不会自动释放此内存,因为不支持GC.
但是这种泄漏的后果是不可预测的,并且因应用程序和应用程序之间的机器而异.假设由于在一台机器上泄漏而崩溃的应用程序可能在具有更多RAM的另一台机器上正常工作.此外,对于给定机器上的给定应用,由于泄漏引起的崩溃可能在运行期间的不同时间出现.
如果泄漏内存,执行就好像没有任何反应一样.这是定义的行为.
在轨道下,您可能会发现malloc由于没有足够的可用内存而导致呼叫失败.但这是一个定义的行为malloc,后果也很明确: malloc呼叫返回NULL.
现在,这可能会导致程序无法检查结果是否因malloc分段违规而失败.但是,由于程序解除引用无效指针,而不是早期的内存泄漏或失败的malloc调用,因此未定义的行为(来自语言规范的POV).
我对这句话的解释:
对于具有非平凡析构函数的类类型的对象,程序不需要在重用或释放对象占用的存储之前显式调用析构函数; 但是,如果没有显式调用析构函数或者如果没有使用delete-expression(5.3.5)来释放存储,则不应该隐式调用析构函数,并且任何程序都依赖于析构函数产生的副作用有未定义的行为.
如下:
如果你以某种方式设法释放对象占用 的存储空间而不调用占用内存的对象上的析构函数,那么如果析构函数是非平凡的并且具有副作用,那么UB就是结果.
如果new分配malloc,原始存储可以释放free(),析构函数将不会运行,并导致UB.或者,如果指针被强制转换为不相关的类型并被删除,则释放内存,但运行错误的析构函数UB.
这与省略delete的基础内存未被释放的情况不同.省略delete不是UB.
(下面评论“注意:这个答案已从/sf/ask/1689590451/ 移至此处” - 您可能需要阅读这个问题是为了获得这个答案的正确背景O_o)。
\n在我看来,标准的这一部分明确允许:
\n拥有一个自定义内存池,您可以将new对象放置到其中,然后释放/重用整个内存池,而无需花费时间调用它们的析构函数,只要您不依赖于对象析构函数的副作用。
分配一点内存但从不释放它的库,可能是因为它们的函数/对象可以由静态对象的析构函数和注册的退出处理程序使用,并且不值得购买整个编排顺序 -每次发生这些访问时都会发生破坏或短暂的“凤凰”般的重生。
\n我不明白为什么当存在副作用依赖性时,标准选择保留行为未定义(直到 C++23) - 而不是简单地说这些副作用不会发生并让程序定义或在这个前提下,未定义的行为正如您通常期望的那样。
\n我们仍然可以考虑标准所说的未定义行为。关键部分是:
\n\n\n“取决于析构函数产生的副作用具有未定义的行为。”
\n
注意:该措辞在 C++23 中被删除,参见[basic.life] p5
\n标准\xc2\xa71.9/12明确定义了副作用如下(下面斜体为标准,表示引入正式定义):
\n\n\n访问由泛左值(3.10)指定的对象
\nvolatile、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数都是副作用,即执行环境状态的变化。
在您的程序中,没有依赖性,因此没有未定义的行为。
\n可以说与 \xc2\xa73.8 p4 中的场景相匹配的依赖关系示例是:
\nstruct X\n{\n ~X() { std::cout << "bye!\\n"; }\n};\n\nint main()\n{\n new X();\n}\nRun Code Online (Sandbox Code Playgroud)\n人们争论的一个问题是,X上面的对象是否会被考虑released用于 3.8 p4 的目的,因为它可能只在程序终止后才释放到操作系统 - 通过阅读标准并不清楚该阶段是否进程的“生命周期”在标准的行为要求范围内(我对标准的快速搜索并没有澄清这一点)。我个人认为 3.8p4 在这里适用,部分原因是只要它足够模糊,编译器编写者可能会觉得有权在这种情况下允许未定义的行为,但即使上面的代码不这样做构成发布场景的容易修改的阿拉...
int main()\n{\n X* p = new X();\n *(char*)p = \'x\'; // token memory reuse...\n}\nRun Code Online (Sandbox Code Playgroud)\n无论如何,但是 main 实现的上面的析构函数有一个副作用- 根据“调用库 I/O 函数”;此外,程序的可观察行为可以说“依赖于”它,因为如果程序运行的话,将受到析构函数影响的缓冲区在终止期间被刷新。但是“取决于副作用”是否仅意味着如果析构函数不运行,程序显然会出现未定义的行为?我倾向于前者,特别是因为后一种情况不需要标准中的专门段落来记录该行为未定义。这是一个明显未定义行为的示例:
\nint* p_;\n\nstruct X\n{\n ~X() { if (b_) p_ = 0; else delete p_; }\n bool b_;\n};\n\nX x{true};\n\nint main()\n{\n p_ = new int();\n delete p_; // p_ now holds freed pointer\n new (&x){false}; // reuse x without calling destructor\n}\nRun Code Online (Sandbox Code Playgroud)\n当x在终止期间调用 \ 的析构函数时,将因此b_对于false已释放的指针,创建未定义的行为。如果在重用之前调用,则将被设置为 0 并且删除将是安全的。从这个意义上说,程序的正确行为可以说取决于析构函数,并且该行为显然是未定义的,但是我们是否刚刚编写了一个与 3.8p4 所描述的行为本身相匹配的程序,而不是该行为是 3.8p4 的结果...?~X()delete p_x.~X();p_
更复杂的问题场景 - 太长而无法提供代码 - 可能包括例如一个奇怪的 C++ 库,其文件流对象内带有引用计数器,必须命中 0 才能触发某些处理,例如刷新 I/O 或加入后台线程等。 -如果不执行这些操作,不仅会导致无法执行析构函数显式请求的输出,而且还会无法从流中输出其他缓冲输出,或者在某些具有事务性文件系统的操作系统上可能会导致早期 I/O 的回滚 -此类问题可能会改变可观察到的程序行为,甚至导致程序挂起。
\n注意:没有必要证明任何实际代码在任何现有的编译器/系统上行为异常;标准明确保留编译器具有未定义行为的权利......这才是最重要的。这不是您可以推理并选择忽略标准的事情 - 可能是 C++14 或其他一些修订版更改了此规定,但只要它存在,那么如果甚至可以说有一些“ “依赖性”对副作用的依赖,那么就有可能出现未定义的行为(当然,它本身可以由特定的编译器/实现来定义,因此它并不自动意味着每个编译器都有义务做一些奇怪的事情)。
\n| 归档时间: |
|
| 查看次数: |
1898 次 |
| 最近记录: |