我应该释放那些通常在程序末尾才会被释放的长期内存吗?

3
我正在编写一个库,将一些结构化的二进制数据解析成一组对象。这些对象预计会比任何用户代码更长寿,并且通常会在主函数结束或之后释放。
我使用共享(和弱)指针来管理每个对象的内存,但这会给程序带来很多额外的复杂性,并引发一些结构问题,在这个问题中不会进一步讨论。
考虑到:
- 遍历整个二进制数据是昂贵的,我不能承受多次遍历。 - 每个访问的条目用于构建一个对象,然后将其注册(即添加到集合中)。 - 二进制数据中的条目可能依赖于稍后出现但立即解析并在再次访问时注册的其他条目。 - 重复的条目可能随时出现,但我需要将这些重复项合并为一个实例(并更新任何指向这些重复项的指针以引用新合并的条目)后进行注册。 - 所有这些对象都保证是派生自一个公共类的许多POD类型之一,因此除了内存之外,不需要清理任何内容。 - 结果程序将在现代操作系统上运行(或在这种情况下,从死亡进程中收集内存)。
我非常想只使用原始指针,永远不释放这些对象占用的内存,并让操作系统在进程退出后进行清理。最好的做法是什么?

从你的问题中并不明显为什么你需要动态分配对象。你不能直接将POD类型放入容器中吗?有没有很好的理由? - Chris Drew
@ChrisDrew 哦,是的,我没有提到所有这些对象都是不同类型的(尽管它们继承自一个共同的基类),所以我需要存储指针而不是实际对象。 - Snaipe
1
一个POD类型不能有虚析构函数,没有虚析构函数就无法使用基类指针删除它。 - Chris Drew
8个回答

4

如果你正在编写可重用代码,则至少需要提供清理选项。假设某个程序使用您的库进行一项操作,然后继续运行,那么安全起见,不能假定进程在您的库完成任务后立即退出。


3
其他答案已经覆盖了一般和标准的方法:在理想情况下,是的,你会清理内存,因为它使代码更通用、更可重用,并有助于工具化。正如其他人所说,对于拥有指针应使用std::unique_ptr,对于非拥有指针应使用原始指针。

还有一些更专业的方法可能有用,也可能没有:

  • 使用池分配器(例如Boost.Pool或自己实现)来预先分配一堆内存,然后为你的对象分配其中的一部分。之后通过删除池可以立即释放每个对象。
  • 故意不释放内存偶尔是一种有效的技术。请参见 Walter Bright 的 “增加编译器速度超过 75%”。当然,编译器是一个专门的问题领域,Walter Bright 可能是目前最顶尖的编译器开发者之一,因此他的问题领域适用的技术不应该被盲目地应用到其他地方。

2
  • 生成的程序将在现代操作系统上运行(或者在这种情况下,从死亡进程中收集内存)。

我很想使用原始指针,永远不释放这些对象占用的内存,并让操作系统在进程退出后进行清理。

如果您采用这种方法,那么任何使用您的库并使用valgrind来检测其程序中的内存泄漏的人都会报告来自您的库的大量泄漏,并向您抱怨,所以如果我是您,我绝对不会这样做。


我知道这一点,但我仍然可以为这些对象提供valgrind抑制,因此它仍然可用。 - Snaipe
如果不考虑用户可能请求这些对象的实例以及某些对象本身引用(而不拥有)其他同类对象的情况,std::unique_ptr本来是可以的。 - Snaipe
最终,我同意这可能会是太大的麻烦,但在选择最好的方案之前,我还在等待更多的答案和想法 :) - Snaipe
只要内存仍然可访问(而且听起来应该是这样),那么Valgrind就不应该抱怨,对吧? - Josh Kelley
@JoshKelley:你知道吗,你说的可能是对的。我确实在代码中看到过这样的情况,例如有人在文件作用域中使用std::map<std::string, static_init_object*>来保存一些静态初始化的注册表,然后发生的情况是该映射在程序结束时被销毁,并且所有这些指针都被valgrind报告为泄漏。但是,也许如果你只有static_init_object * ptr,它并没有像堆栈变量一样被“释放”,因此在程序结束时它看起来不像是一个泄漏?我想我应该进行实验以找出答案,我不确定。 - Chris Beck
显示剩余2条评论

1
如果您正在编写一个库,那么应该提供一个清理函数来释放您分配的所有内存。一个实际的例子是,如果Windows DLL使用了您的库。当库被加载时,静态数据被初始化。当库被卸载时,静态数据被清除。如果您的库有一些指向从不释放的内存的全局指针,则DLL的加载卸载周期将导致内存泄漏。

2
OpenSSL也有这个漏洞。我曾经提交过一个补丁,添加了一个清理函数,但被拒绝了,因为有人说这些东西永远不应该被“释放”... 真是让人费解。 - M.M

1
如果您的所有权和生存期明确,我建议您使用unique_ptr来管理拥有指针,并使用原始指针来管理非拥有指针。相比于shared_ptrweak_ptr,这应该更简单,同时仍然可以自动管理内存。
我认为完全不管理内存不是一个选项。但我认为使用智能指针表达所有权不仅关乎良好的内存管理,还使代码更易于理解。

1
如果对象都是相同类型的,那么你可以将它们全部放入一个向量中,通过索引号相互引用而不是使用指针来分配每个对象。向量的内置内存管理会在需要时处理分配空间,当你完成对象操作后,只需销毁向量以一次性释放所有对象。(请注意,vector::clear()并不实际释放内存,但确实使其可用于在向量中存储新的对象集。)
如果你的对象不是相同类型的,你需要了解更一般的基于区域的内存管理概念。与上述类似,你可以在相对较少的内存块(可能只有一个)中分配所有对象,稍后可以自由地释放这些内存块,而无需跟踪分配其中的所有单个对象。

0

尝试考虑未来的维护工作。假设您的代码需要在以后进行拆分或其他操作。在这种情况下,您将面临泄漏或成为资源猪的风险。


0

清理(或能够进行清理)是很好的。现在看来,一个应用程序应该在其整个生命周期内使用单个结构化二进制数据集,这似乎是显而易见的,但一旦你意识到需要编写一个需要在半路重置并重新开始使用另一个数据集的应用程序时,你会开始后悔。

(一个相关的容易被忽视的事情是,一个应用程序可能需要同时处理两个完全独立的数据集,因此尽量不要设计你的库排除这种情况!)


话虽如此,我认为你可能过于关注极端情况了。不应参与内存管理的代码可以使用原始指针,在没有这些指针超出内存中结构化数据集的风险时,这是合理的。

然而,这并不意味着参与内存管理的代码也需要使用原始指针。即使您向用户传递原始指针,也可以使用智能指针来管理数据结构。

除此之外,请记住,根据我的经验,指针通常是错误的语义——通常,大多数用例最自然的是引用或值语义,这意味着您应该传递原始引用,或传递具有引用或值语义的轻量级包装类,但实现为包含指向实际数据的指针。如果适当的话,甚至可以作为实际数据的副本。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接