C++内存管理范式

12
我正在从C转向C++11,并试图找出C++11程序(或任何具有内置异常的现代语言)的内存管理范例。特别是,我正在尝试进行游戏开发,在其中内存耗尽是一个真正的问题。
在C中,我使用检查malloc的返回值;并通常使用自定义分配器。
对于C ++,我感到相当困惑;尽管我喜欢STL容器的构建方式,允许自定义分配器。由于STL容器都管理自己的内存,因此仅向向量添加元素就可能引发std :: bad_alloc。我该如何防范这种情况?我听说将所有抛出调用包装在try / catch块中可能会导致成本过高。
然而,允许异常传播到调用堆栈中将允许一堆函数无法完全执行,并且将导致一些非常棘手的代码。即如果A->B->C->D是调用堆栈,则D抛出异常,A捕获,则B、C和D可能通过无法正常完成执行而创建一些奇怪的问题。
此外,nothrow参数似乎允许非常类似C的代码;但我现在看不出与普通malloc相比的好处。
有哪些最佳实践可以编写异常安全的C++代码以保护免受内存不足问题的影响? 编辑:在progammers.stackexchange上提供了一个相关答案,主张在控制台中设计无异常的C++。不确定这些论点是否仍适用于第8代游戏机

你知道某些平台会愉快地从malloc()返回一个非空指针,但一旦你实际使用该内存,它仍然会失败(因为虚拟内存处理程序只有在然后意识到它已经用完了交换空间)? - DevSolar
@DevSolar 是的,我已经听说过这种可能性。幸运的是,我从未遇到过这个问题——不确定在这种情况下我该怎么做,除了保留足够大的缓冲区来处理这种情况以便有一些余地来适当退出。 - zac
2个回答

8
我的回答将更偏向于游戏开发,因为这是我的背景之一,也是你感兴趣的部分。不同类型的应用程序将有不同的要求。
游戏通常会预先分配所有动态内存并在其预算范围内运行。特别是主机有严格的内存限制,大多数游戏都希望使用全部内存。
有几个原因需要预先分配所有内容。
首先是性能。内存分配很慢,你要尽量避免。如果你预先分配了所有内容,你可以编写定制的高性能内存分配器,如池分配器、堆栈分配器等,只需从预先分配的缓冲区中获取内存。选择最适合当前任务的分配器非常重要。
其次,你会很快知道游戏是否有足够的内存。在开发过程中,如果内存用完了,你就会崩溃并需要调整使用情况,但最终发布时不应该崩溃,因为你已经预先分配并在内存预算范围内运行。
对于异常,许多游戏(但并非全部)为了性能原因禁用异常。实际上,一些控制台编译器甚至不支持异常。那么你要么需要使用没有异常的STL库,要么实现自己的容器。许多游戏团队选择实现自己的容器,既出于性能原因,也为了更好地将它们与自定义内存分配器集成。
话虽如此,动态内存分配、STL和异常对于较小的个人项目/游戏可能完全没问题,但请记住,在大型、高性能、实时游戏中所需的内容。
对于异常安全性,我肯定会使用RAII。这就是它的目的。此外,我建议使用智能指针,如std::unique_ptrstd::shared_ptr进行内存管理。结合RAII,如果您的构造函数抛出异常,内存将被释放。

1
我还建议阅读有关电子艺界STL的内容:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2271.html - kevinarpe
1
出于性能原因禁用异常是愚蠢的。如果正确处理,异常可以胜过错误代码(C方式)来处理错误。 - BЈовић
1
@megadan 是的,我刚刚在研究这个。异常似乎在异常情况下非常慢 - 即使使用“零成本”异常,在正常情况下仍会增加一些轻微的开销。 - zac
1
@Rob K 我的评论是针对游戏开发的。性能关键代码不会充斥着错误码和返回值检查,就像它不能充满抛出异常一样。发布的游戏不应该失败。如果它们失败了,通常意味着它们必须立即退出。大多数故障必须在开发过程中处理。 - megadan
1
kevinarpe发布的关于EA的文章中有一个很好的部分,介绍了如何在游戏中禁用异常处理。这是来自EA自己的建议。再次强调,这仅适用于游戏开发领域,并不一定适用于其他领域的最佳实践。 - megadan
显示剩余5条评论

6
使用析构函数在作用域退出时自动清理资源。这被称为RAII,即“Resource Acquisition Is Initialization”,尽管该首字母缩略词不是最佳选择。所有标准容器等都会自动清理。
在像C#和Java这样基于垃圾收集的语言中,您需要在代码中使用try块和“using”语句。Java刚刚得到了它(我IRC的一部分);从一开始就有C#(关键字using);在Python中,它称为with;而C ++没有它,也不需要它。我曾经为C++创建过一个WITH宏,这是一个聪明的小技巧,认为我会一直使用它,但除了创建后立即尝试一次之外,我从未使用过它:在C ++中,RAII已经做到了这一切。
总结:使用RAII,即使用析构函数,并让这些异常传播。
关于内存耗尽,通常只被视为“我们已完成”,除了以尽可能有序的方式终止外,没有其他事情可做。
但最好设置一些缓冲区,当内存耗尽时可以释放,以便进行一些清理工作的工作内存。
C ++不区分硬异常(例如内存耗尽)和软异常(一般性失败,不是致命的)。

+1. 这个解释了问题A->B->C-D中的调用栈是如何被清理的:即使D抛出异常,D、C和B中的析构函数仍然会运行。这相当于Java的finally语句块,但在C++中,清理是在每种类型中指定的,而不是在使用该类型的每个函数中指定的。这是一种净节省。 - MSalters
同意,我喜欢RAII的想法。感谢您的帖子。干杯! - zac
1
RAII比使用finally块更好的另一件事情是,没有RAII,每次你使用一个对象实例的地方都必须有一个finally块来完成完全相同的清理工作,这违反了“不要重复自己(DRY)”原则。 - Rob K

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