为什么创建STL容器动态地被认为是不好的做法?

时间:2011-02-20 23:53:15

标签: c++ stl

标题说。

糟糕的样本:

std::vector<Point>* FindPoints()
{
   std::vector<Point>* result = new std::vector<Point>();
   //...
   return result;
}

如果我稍后删除vector会有什么问题?

我主要用C#编程,所以在C ++上下文中这个问题对我来说并不是很清楚。

6 个答案:

答案 0 :(得分:11)

根据经验,您不这样做,因为您在堆上分配的越少,泄漏内存的风险就越小。 :)

std::vector也很有用,因为它以RAII方式自动管理用于向量的内存;现在,通过在堆上分配它,您需要显式释放(使用delete result)以避免泄漏其内存。由于例外,事情变得复杂,这可能会改变您的返回路径并跳过您放置的任何delete。 (在C#中,您没有这样的问题,因为垃圾收集器会定期调用无法访问的内存)

如果要返回STL容器,您有几种选择:

  • 只需按值返回;从理论上讲,由于在返回result的过程中产生的临时性,你应该产生一个复制品,但是较新的编译器应该能够使用NRVO 1来删除副本。 SUP>。可能还有std::vector个实现,像许多std::string实现那样实现了写时复制优化,但我从未听说过。

    在C ++ 0x编译器上,移动语义应该触发,避免任何副本。

  • 将结果指针存储在所有权转移智能指针中,如std::auto_ptr(或C ++ 0x中的std::unique_ptr),并将函数的返回类型更改为std::auto_ptr<std::vector<Point > > ;这样,你的指针总是封装在一个堆栈对象中,当函数退出时(以任何方式)会自动销毁,如果它仍由它拥有,则会销毁vector。此外,完全清楚谁拥有返回的对象。
  • 使result向量成为调用者通过引用传递的参数,并填充该向量而不是返回新向量。
  • Hardcore STL选项:您可以将数据作为迭代器提供;然后,客户端代码将使用std::copy + std::back_inserter或其他任何内容将这些数据存储在所需的容器中。看不多(代码正确可能很棘手),但值得一提。

  1. 正如@Steve Jessop在评论中指出的那样,只有在返回值直接用于初始化调用方法中的变量时,NRVO才能完全工作;否则,它仍然可以忽略临时返回值的构造,但仍然可以调用为其分配返回值的变量的赋值运算符(有关详细信息,请参阅@Steve Jessop的注释)。

答案 1 :(得分:6)

动态创建任何东西都是不好的做法,除非真的有必要。很少有理由动态创建容器,因此通常不是一个好主意。

编辑:通常,不要担心返回容器的速度有多快或多慢,而是大多数代码只应处理容器中的迭代器(或两个)。

答案 2 :(得分:2)

一般情况下动态创建对象在C ++中被认为是一种不好的做法。如果您的“// ...”代码抛出异常怎么办?你将永远无法删除该对象。简单地做起来更简单,更安全:

std::vector<Point> FindPoints()
{
  std::vector<Point> result;
  //...
  return result;
} 

更短,更安全,更直接......至于性能,现代编译器会在返回时优化掉副本,如果它们不能,移动构造函数将被执行,因此这仍然是一个廉价的操作。

答案 3 :(得分:0)

也许您指的是最近的这个问题:C++: vector<string> *args = new vector<string>(); causes SIGABRT

一个班轮:这是不好的做法,因为它是一种容易出现内存泄漏的模式。

您强制调用者接受动态分配并负责其生命周期。返回指针返回的是静态缓冲区,某个其他API(或对象)拥有的缓冲区,或者调用者现在拥有的缓冲区,这是不明确的。你应该避免使用任何语言(包括普通C语言)的这种模式,除非从函数名中清楚地知道发生了什么(例如strdup,malloc)。

通常的方法是这样做:

void FindPoints(std::vector<Point>* ret) {
   std::vector<Point> result;
   //...
   ret->swap(result);
}

void caller() {
  //...
  std::vector<Point> foo;
  FindPoints(&foo);
  // foo deletes itself
}

所有对象都在堆栈中,所有删除都由编译器处理。或者只是按值返回,如果你正在运行C ++ 0x编译器+ STL,或者不介意副本。

答案 4 :(得分:0)

我喜欢Jerry Coffin的回答。此外,如果要避免返回副本,请考虑将结果容器作为引用传递,有时可能需要使用swap()方法。

void FindPoints(std::vector<Point> &points)
{
    std::vector<Point> result;
    //...
    result.swap(points);
}

答案 5 :(得分:0)

编程是找到妥协的艺术。动态分配的内存当然可以占有一席之地,我甚至可以考虑使用std::vector<std::vector<T>*>获得代码复杂性和效率之间良好折衷的问题。

但是std::vector可以很好地隐藏动态分配数组的大多数需求,而托管指针很多时候只是动态分配单个实例的完美解决方案。这意味着发现非托管动态分配容器(或实际上动态分配任何内容)是C ++中最佳折衷方案的情况并不常见。

在我看来,这不会使动态分配变得“糟糕”,但如果你在代码中看到它就会“怀疑”,因为很有可能找到更好的解决方案。

例如,在您的情况下,我认为没有理由使用动态分配;只是使函数返回std :: vector将是高效和安全的。在分配给新声明的向量时,将使用任何体面的编译器Return Value Optimization,如果需要将结果分配给现有向量,您仍然可以执行以下操作:

FindPoints().swap(myvector);

不会对数据进行任何复制,只会对某些指针进行复杂处理(请注意,由于C ++规则有时令人讨厌禁止将临时值作为非const引用传递,因此您无法使用明显更自然的myvector.swap(FindPoints()) )。

根据我的经验,动态分配对象的最大需求来源是复杂的数据结构,其中可以使用多个访问路径到达相同的实例(例如,实例同时在双向链表中并由地图索引) 。在标准库中,容器始终是包含对象的唯一所有者(C ++是一种复制语义语言),因此如果没有指针和动态分配概念,可能很难有效地实现这些解决方案。

通常情况下,您可以使用标准容器进行合理足够的妥协(可能需要支付一些您可以避免的额外O(log N)查找)并且考虑到更简单的代码,可以将IMO作为最佳折衷方案在大多数情况下。