Answers:
因为无法确定程序是否会再次使用内存。这意味着free()
在所有情况下,没有算法可以正确确定何时调用,这意味着任何尝试执行此操作的编译器都必然会产生一些内存泄漏程序和/或某些继续使用已释放内存的程序。即使您确保编译器从不执行第二个编译器,并且允许程序员插入调用以free()
修复这些错误,知道何时调用free()
该编译器比free()
使用不尝试的编译器知道何时调用更加困难。帮助。
free()
正确插入。
正如大卫·里希比(David Richerby)正确指出的那样,这个问题通常无法确定。对象活动性是程序的全局属性,通常可能取决于程序的输入。
甚至精确的动态 垃圾回收也是一个无法确定的问题!所有现实生活中的垃圾收集器都将可达性用作未来是否需要分配的对象的保守估计。这是一个很好的近似值,但是仍然是一个近似值。
但这通常是正确的。在计算机科学领域,最臭名昭著的解决方案之一是“总的来说这是不可能的,因此我们无能为力”。相反,在许多情况下可以取得一些进展。
基于引用计数的实现非常接近“编译器插入释放”,因此很难分辨出区别。LLVM的自动引用计数(在Objective-C和Swift中使用)是一个著名的例子。
区域推断和编译时垃圾收集是当前活跃的研究领域。事实证明,使用声明性语言(如ML和Mercury)要容易得多,在这些语言中,创建对象后便无法对其进行修改。
现在,在人员问题上,人员可以通过三种主要方式手动管理分配生存期:
诚然,释放分配语句的最佳位置是不确定的,但这根本不是问题。由于对于人类和编译器来说都是无法确定的,因此,无论是手动还是自动处理,始终无法有意识地选择最佳的解除分配位置是不可能的。而且由于没有人能做到完美,因此在猜测近似最佳位置时,足够先进的编译器应能胜过人类。因此,不确定性不是我们需要显式释放语句的原因。
在某些情况下,外部知识会影响释放语句的放置。删除这些语句等效于删除部分操作逻辑,而要求编译器自动生成该逻辑等效于要求其猜测您的想法。
例如,假设您正在编写一个Read-Evaluate-Print-Loop(REPL):用户键入命令,然后程序将执行该命令。用户可以通过在REPL中键入命令来分配/解除分配内存。您的源代码将指定REPL对每个可能的用户命令应执行的操作,包括在用户键入命令时进行的重新分配。
但是,如果C源代码未提供用于释放的显式命令,则编译器将需要推断,当用户将适当的命令输入到REPL中时,编译器应执行释放。该命令是“解除分配”,“免费”还是其他?编译器无法知道您想要的命令是什么。即使您进行逻辑编程以查找该命令字并且REPL找到了该命令字,除非您在源代码中明确告知它,否则编译器也无法知道它应该通过释放来响应它。
tl; dr 问题是C源代码不向编译器提供外部知识。不确定性不是问题,因为无论是手动还是自动化过程都存在不确定性。
目前,没有任何答案是完全正确的。
编译器为何不自动插入解除分配?
有的。(我稍后再解释。)
通常,您可以free()
在程序退出之前调用。但是您的问题中隐含需要free()
尽快致电。
free()
一旦存储器不可达,何时调用任何C程序的问题是无法确定的,即,对于在有限时间内提供答案的任何算法,都存在无法解决的情况。这-以及任意程序的许多其他不确定性-可以从暂停问题中得到证明。
不确定的问题无法始终通过任何算法(无论是编译器还是人工算法)在有限时间内解决。
人类(尝试)写在一个子集 C程序是可以通过他们的算法(自己)来验证内存正确性。
某些语言通过将#5构建到编译器中来实现#1。它们不允许程序任意使用内存分配,而是允许确定的子集。Foth和Rust是语言的两个示例,它们比C语言具有更多的限制内存分配malloc()
,可以(1)检测程序是否在其可决定的集合之外编写(2)自动插入释放。
“人类做到这一点,所以这并非不可能”是众所周知的谬论。我们不一定了解(更不用说控制)我们创造的东西了,金钱就是一个常见的例子。我们倾向于高估(有时是大幅度地)在技术问题上取得成功的机会,尤其是在似乎缺少人为因素的情况下。
人类在计算机编程中的表现非常差,计算机科学的研究(许多专业教育计划都缺乏)有助于理解为什么这个问题没有简单的解决方法。我们也许有一天,也许不是很遥远,被工作上的人工智能所取代。即使那样,也不会有一直自动地正确进行重新分配的通用算法。
问题主要是历史性的产物,而不是不可能的实现。
大多数C编译器生成代码的方式是使编译器一次只能看到每个源文件。它永远不会看到整个程序。当一个源文件从另一源文件或库中调用函数时,编译器看到的只是带有函数返回类型的头文件,而不是函数的实际代码。这意味着,当有一个返回指针的函数时,编译器无法告诉指针所指向的内存是否需要释放。决定当时未显示给编译器的信息。另一方面,人类程序员可以自由地查找函数或文档的源代码,以找出需要使用指针执行的操作。
如果您研究更现代的低级语言(例如C ++ 11或Rust),您会发现它们主要通过在指针类型中明确显示内存所有权来解决此问题。在C ++中,您将使用a unique_ptr<T>
而不是Plain T*
来保存内存,并unique_ptr<T>
确保当对象到达作用域的末尾时释放内存,这与Plain不同T*
。程序员可以将内存从一个unique_ptr<T>
转移到另一个,但是永远只能有一个unique_ptr<T>
指向该内存。因此,始终很清楚谁拥有内存以及何时需要释放内存。
由于向后兼容的原因,C ++仍然允许使用旧样式的手动内存管理,因此可以创建错误或方法来绕过对a的保护unique_ptr<T>
。Rust更加严格,因为它通过编译器错误来强制执行内存所有权规则。
至于不确定性,暂停问题等等,是的,如果您坚持使用C语义,则不可能为所有程序决定何时释放内存。但是,对于大多数实际程序,而不是学术练习或越野车软件,绝对可以决定何时免费和何时不收费。毕竟,这是人类首先可以弄清楚何时应该自由的唯一原因。
其他答案集中在是否可以进行垃圾回收,如何完成垃圾回收的一些细节以及一些问题上。
一个尚未解决的问题是垃圾收集不可避免的延迟。在C语言中,当程序员调用free()时,该内存可立即用于重用。(至少在理论上是这样!)因此,程序员可以释放其100MB结构,在一毫秒后分配另一个100MB结构,并期望总体内存使用保持不变。
对于垃圾回收,情况并非如此。垃圾收集系统在将未使用的内存返回到堆时会有一些延迟,这可能很重要。如果您的100MB结构超出范围,并且在一毫秒后您的程序设置了另一个100MB结构,则可以合理地期望您的系统在短时间内使用200MB。取决于系统,该“短时间段”可能是毫秒或秒,但仍然存在延迟。
如果您在具有大量RAM和虚拟内存的PC上运行,那么您可能永远不会注意到这一点。但是,如果您在资源更有限的系统(例如嵌入式系统或电话)上运行,则需要认真对待。这不仅是理论上的-我个人已经看到在使用.NET Compact Framework的WinCE系统上工作并使用C#开发时,这会造成问题(例如使设备崩溃)。
这个问题假定一个重新分配是程序员应该从源代码的其他部分推导出来的。不是。“在程序的这一点上,内存引用FOO不再有用”,是只有在程序员将其编码为(使用过程语言)释放语句后,才在程序员心中知道的信息。
从理论上讲,它与其他任何代码行都没有什么不同。为什么编译器不自动插入“此时,在程序中,检查寄存器BAR以进行输入”或“如果函数调用返回非零,则退出当前子例程”?从编译器的角度来看,原因是“不完整”,如此答案所示。但是,当程序员没有告诉他所知道的一切时,任何程序都会遭受不完整的困扰。
在现实生活中,解除分配是艰巨的工作或样板。我们的大脑会自动填充它们并对此抱怨,并且“编译器可以做得更好或更好”的想法是正确的。从理论上讲,事实并非如此,尽管幸运的是其他语言为我们提供了更多的理论选择。
什么是做:有垃圾收集,并有使用引用计数(Objective-C中,斯威夫特)编译器。那些进行引用计数的程序需要避免强大的引用周期,从而需要程序员的帮助。
对“为什么” 的真正答案是,编译器作者尚未想出一种足够好,足够快的方法来使其在编译器中可用。由于编译器编写者通常很聪明,因此您可以得出结论,很难找到一种足够好且足够快的方法。
它非常非常困难的原因之一当然是不确定。在计算机科学中,当我们谈论“可确定性”时,是指“做出正确的决定”。人类程序员当然可以轻松地决定将内存分配到哪里,因为他们不仅限于正确的决定。而且他们经常做出错误的决定。
在像C这样的语言中,程序员应该插入免费的调用。为什么编译器不自动执行此操作?
在C语言和许多其他语言中,确实存在一种工具,可以使编译器在编译时明确应完成的情况下做到与之等效:使用自动持续时间变量(即普通局部变量) 。编译器负责为此类变量安排足够的空间,并负责在其(明确定义的)生存期结束时释放该空间。
自C99以来,可变长度数组一直是C的功能,因此,原则上,自动持续时间对象实际上可用于C中所有可计算持续时间的动态分配对象所具有的功能。当然,在实践中,C 实现可能会对VLA的使用施加很大的实际限制-即,其大小可能由于在堆栈上分配而受到限制-但这是实现方面的考虑,而不是语言设计上的考虑。
那些预期用途无法为其提供自动持续时间的对象正是那些在编译时无法确定其寿命的对象。