C++中的内存管理模式

Memory management patterns in C++

本文关键字:管理模式 内存 C++      更新时间:2023-10-16

我认为我对正常(功能)设计的模式有相当多的经验,例如在我主要在java和C#中使用的《四人帮》一书中所描述的。在这些"托管"语言中,这几乎是你完成工作所需要知道的一切。

然而,在C++世界中,开发人员还可以控制如何分配、传递和删除所有对象。我理解这些原理(我阅读了Stroutrup和其他文本),但我仍然需要付出很多努力来决定哪种机制最适合给定的场景——这就是与记忆相关的设计模式组合的有用之处。

例如,昨天我不得不创建一个类Results,它是一些对象的容器,也是另一种类型对象的集合(在本例中为std::vector)。因此,有几个设计问题我无法真正回答:

  1. 我应该按值还是按智能指针返回这个类
  2. 在类内部,向量和对象应该是普通成员,还是应该再次作为智能指针存储
  3. 在向量中,我应该直接存储对象,还是再次存储指向它们的智能指针
  4. 在Results类上定义的getter应该返回什么(即值、引用或智能指针)

当然,智能指针很酷,但它们会造成语法混乱,我不相信对每个对象使用malloc是否是最佳方法。

我很感激上面具体几点的答案,但更感谢一些关于记忆相关设计模式的更长、更一般的文本,这样我也可以解决周一的问题!

所有问题的答案都是一样的:这取决于您是需要引用语义还是值语义(需要考虑一些注意事项)。

如果您需要引用语义,这是Java和C#等语言中使用class关键字声明的UDT(用户定义的数据类型)的默认语义,那么您必须使用智能指针。在这种情况下,您希望多个客户端将安全别名保存到特定对象,其中safe一词封装了这两个要求:

  1. 避免悬挂引用,这样你就不会试图访问一个已经不存在的对象
  2. 避免使用比所有引用都要长的对象,这样就不会泄露内存

这就是智能指针所做的。如果你需要引用语义(如果你的算法在需要共享所有权的地方不足以使引用计数的开销显著),那么你应该使用智能指针

例如,当您希望同一个对象是多个集合的一部分时,您确实需要引用语义。更新一个集合中的对象时,希望所有其他集合中同一对象的表示形式都能一致更新。在这种情况下,可以将指向对象的智能指针存储在这些集合中。智能指针封装对象的标识,而不是其值。

但是,如果你不需要创建别名,那么值语义可能是你应该依赖的。当你用自动存储(即在堆栈上)声明一个对象时,这是C++中默认得到的。

需要考虑的一件事是STL集合存储,因此如果您有vector<T>,则T副本将存储在vector中。总是假设您不需要引用语义,如果您的对象很大并且复制成本很高,那么这可能会成为一种开销。

为了限制这种情况的可能性,C++11提供了move操作,当不再需要对象的旧副本时,可以有效地按值传输对象。


我现在将尝试使用上述概念来更直接地回答您的问题

1)我应该按值还是按智能指针返回此类

这取决于您是否需要引用语义。函数对那个对象做什么?该函数返回的对象是否应该由许多客户端共享?如果是,则通过智能指针。如果没有,是否可以定义一个有效的移动操作(几乎总是这样)?如果是,那么按价值计算。如果不是,则通过智能指针。

2)在类内部,向量和对象应该是普通成员,还是应该再次作为智能指针存储

很可能是普通成员,因为向量通常在概念上是对象的一部分,因此它们的生存期与嵌入它们的对象的生存期相关联。在这种情况下,您很少需要引用语义,但如果需要,请使用智能指针。

3)在向量中,我应该直接存储对象,还是再次存储指向它们的智能指针

答案与第1点相同:你需要共享这些对象吗?你应该存储这些对象的别名吗?是否希望在引用这些对象的代码的不同部分中看到对这些对象的更改?如果是,则使用共享指针。如果没有,是否可以有效地复制和/或移动这些对象?如果是(大多数情况下),请存储值。如果不是,则存储智能指针。

4)在Results类上定义的getter应该返回什么(即值、引用或智能指针)

答案与第2点相同:这取决于您计划如何处理返回的对象:您希望代码的许多部分共享它们吗?如果是,则返回一个智能指针。如果它们仅由一部分独家所有,则按价值返还,除非移动/复制这些对象过于昂贵或根本不允许(非常不可能)。在这种情况下,返回一个智能指针。


顺便说一句,请注意C++中的智能指针比Java/C#引用有点棘手:首先,根据需要共享所有权(shared_ptr)还是唯一所有权指针(weak_ptr)存在的原因。

这些概念自然导致了责任的概念,用于管理对象的生命周期或(更普遍地)管理已使用的资源。例如,您可能想了解RAII习惯用法(资源获取是初始化),以及一般的异常处理(编写异常安全代码是这些技术存在的主要原因之一)。