【问题标题】:Memory management patterns in C++C++ 中的内存管理模式
【发布时间】:2013-01-10 11:14:50
【问题描述】:

我认为我对正常(功能性)设计模式有相当多的经验,如所述,例如在the gang of four book,我主要用在java和C#中。在这些“托管”语言中,这几乎是您完成工作所需了解的所有内容。

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

例如,昨天我必须创建一个类Results,它是一些对象的容器和另一种对象的集合(在本例中为std::vector)。所以有一些设计问题我无法真正回答:

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

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

我将不胜感激以上具体点的答案,但更希望获得一些关于内存相关设计模式的更长、更笼统的文本——这样我也可以解决周一遇到的问题!

【问题讨论】:

  • 您听说过 RAII 吗?从您的问题来看,如果您熟悉 RAII,似乎还不够清楚,而且您正在谈论的事情超出了它。是这样吗?
  • Get Effective C++ By Scott Meyers 里面有一整节是关于 RAII 和内存管理等的。
  • @Nawaz,是的,我熟悉 RAII。我也有 Scotts Meyers 的书。我认为这对这种情况没有任何帮助......

标签: c++ design-patterns memory-management


【解决方案1】:

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

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

  1. 避免悬空引用,这样您就不会尝试访问不再存在的对象;
  2. 避免使用比对它们的所有引用都寿命更长的对象,以免内存泄漏。

这就是智能指针的作用。 如果您需要引用语义(并且如果您的算法不会在需要共享所有权的情况下使引用计数的开销显着增加),那么您应该使用智能指针

您确实需要引用语义,例如,当您希望同一个对象成为多个集合的一部分时。当您更新一个集合中的对象时,您希望所有其他集合中的同一对象的表示得到一致更新。在这种情况下,您将智能指针存储到这些集合中的对象。智能指针封装了一个对象的身份,而不是它的值。

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

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

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


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

1) 我应该通过值还是通过智能指针返回这个类?

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

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

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

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

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

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

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


附带说明,请注意 C++ 中的智能指针比 Java/C# 引用要复杂一些:首先,根据是否共享所有权,您有两种主要的智能指针风格(shared_ptr) 或唯一所有权 (unique_ptr) 是需要的。其次,您需要避免对shared_ptr 的循环引用,这会创建使彼此保持活动状态的对象岛,即使您正在运行的代码无法访问它们。这就是弱指针 (weak_ptr) 存在的原因。

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

【讨论】:

  • C# 同时具有:值语义 UDT 和引用语义 UDT,分别用 structclass 定义。 +1 很好的答案。
  • @Nawaz:感谢您指出这一点,我将编辑我的答案
  • 感谢您提供非常好的答案!在我的特殊情况下,我认为我会将所有这些东西存储为普通成员。我需要对我的类做一些特别的事情来让它们与移动语义一起工作吗?
  • @Grzenio:你需要定义一个move构造函数。但如果你不熟悉这些概念,我会先阅读一些关于 C++11 中移动语义和右值引用的教程(如果不是书籍)。参见例如this very good introductionthis tutorial
  • @Andy:实际上,通常不需要定义移动构造函数。编译器默认的,它简单地为每个子对象调用移动构造函数,通常就足够了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-10
  • 1970-01-01
  • 1970-01-01
  • 2010-09-06
  • 2018-12-17
相关资源
最近更新 更多