【问题标题】:Why is using vector of pointers considered bad?为什么使用指针向量被认为是不好的?
【发布时间】:2015-02-12 03:38:51
【问题描述】:

最近我遇到了一些意见,认为我不应该使用指针向量。 我想知道 - 为什么我不能?

例如,如果我有一个班级 foo,则可以这样做:

vector <foo*> v;
v.push_back(new foo());

我已经看到有人反对这种做法,这是为什么呢?

【问题讨论】:

  • 对象不会自动销毁。为此,请使用 Boost 的 ptr_vectorvector&lt;unique_ptr&lt;&gt;&gt;
  • 我认为这是因为所有 std 容器都会复制它们拥有的所有元素,因此,如果我们在 std::vector 中有一个指针,并且你修改它,你实际上并没有修改初始指针
  • I wanted to know - why I cant? 你可以——但最好使用智能指针。没有上下文,处理不良做法的问题毫无意义。请在问题本身中包含不同意见。
  • 问题不在于使用指针向量。问题是它们指向新对象,不清楚谁拥有这些对象。
  • C++ 人倾向于支持由语言特性强制执行的 RAII 习语。如果您对std::vector 和对象范围有足够的了解,那么就没有问题——但您确实无法通过这种方法充分利用容器和所有权语义。这也使得异常安全性更难保证。

标签: c++ pointers vector std


【解决方案1】:

在容器中存储普通指针会导致内存泄漏和悬空指针。将指针存储在容器中并不定义指针的任何类型的所有权。因此容器不知道析构和复制操作的语义。当从容器中删除元素时,容器不知道如何正确地销毁它们,当执行复制操作时,不知道所有权语义。当然,你总是可以自己处理这些事情,但仍然有可能出现人为错误。

使用智能指针将所有权和销毁语义留给它们。

另外要提到的是容器被划分为non-intrusive and intrusive contaiers - 它们存储实际提供的对象而不是副本,因此它实际上归结为指针集合。非侵入式指针有一些优点,所以你不能概括容器中的指针是任何时候都应该避免的东西,但在大多数情况下还是推荐的。

【讨论】:

  • 我完全不同意这里的所有答案。应该避免这种情况的主要原因是它增加了额外的间接性并减少了缓存局部性。
【解决方案2】:

使用原始指针向量不一定是不好的风格,只要您记住指针没有所有权语义。当您开始使用newdelete 时,通常意味着您做错了什么。

特别是,在现代 C++ 代码中应该使用 newdelete 的唯一情况是在构造 unique_ptr 或使用自定义删除器构造 shared_ptr 时。

例如,假设我们有一个实现双向Graph 的类,Graph 包含一定数量的Vertexes

class Vertex 
{
public: 
    Vertex();
    // raw pointer. No ownership
    std::vector<Vertex *> edges;
}

class Graph 
{
public:
    Graph() {};

    void addNode() 
    {
        vertexes.push_back(new Vertex); // in C++14: prefer std::make_unique<>
    }

// not shown: our Graph class implements a method to traverse over it's nodes
private:
    // unique_ptr. Explicit ownership
    std::vector<std::unique_ptr<Vertex>> vertexes;
}

void connect(Vertex *a, Vertex *b) 
{
    a->edges.push_back(b);  
    b->edges.push_back(a);
}

注意我是如何在Vertex 类中拥有一个原始的Vertex * 向量?我可以这样做,因为它指向的Vertexes 的生命周期由Graph 类管理。我的 Vertex 类的所有权仅通过查看代码就很明确。

另一个答案建议使用 shared_ptr's。我个人不喜欢这种方法,因为通常共享指针很难推断对象的生命周期。在这个特定的示例中,共享指针根本不起作用,因为 Vertexes 之间的循环引用。

【讨论】:

    【解决方案3】:

    因为vector的析构函数不会在指针上调用delete,所以很容易意外泄漏内存。向量的析构函数调用向量中所有元素的析构函数,但原始指针没有析构函数。

    但是,您可以使用智能指针向量来确保销毁该向量将释放其中的对象。 vector&lt;unique_ptr&lt;foo&gt;&gt; 可以在 C++11 中使用,而在带有 TR1 的 C++98 中,您可以使用 vector&lt;tr1::shared_ptr&lt;foo&gt;&gt;(尽管与原始指针或 unique_ptr 相比,shared_ptr 有一点开销)。

    Boost 还有一个pointer container library,其中特殊的delete-on-destruction 行为内置于容器本身中,因此您不需要智能指针。

    【讨论】:

    • 它不会调用delete,但这取决于程序员,不是吗? (我的意思是你必须在删除向量时调用 delete 是必要的)
    • @joppiesaus 管理指针的生命周期要困难得多。即,复制向量。
    • 是的,您可以通过手动删除所有元素来避免内存泄漏,但这可能会很棘手(例如,当涉及异常时),并且很容易忘记或弄错。并不是特别地 不鼓励原始指针的容器,而是一般 中的原始指针不被鼓励。除非您有充分的理由不这样做,否则请使用智能指针。
    【解决方案4】:

    其中一个问题是异常安全

    例如,假设某个地方抛出了异常:在这种情况下,std::vector 的析构函数被调用。但是这个析构函数调用不会删除存储在向量中的原始拥有指针。所以,这些指针管理的资源是泄露的(它们可能都是内存资源,所以你有内存泄漏,但它们也可能是非内存资源,例如套接字、OpenGL 纹理等。 )。

    相反,如果您有一个由智能指针组成的向量(例如std::vector&lt;std::unique_ptr&lt;Foo&gt;&gt;),那么如果调用向量的析构函数,则向量中的每个指向项(由智能指针安全拥有)都是正确删除,调用其析构函数。因此,与每个项目关联的资源(向量中“智能”指向)被正确释放。

    请注意,观察原始指针的向量很好(假设观察到的项目的生命周期超过了向量的生命周期)。问题在于 raw 拥有 指针。

    【讨论】:

      【解决方案5】:

      我将专门讨论一个指针向量,它负责管理指向对象的生命周期,因为这是唯一一个指针向量显然是有问题的选择的情况。

      还有更好的选择。具体来说:

      std::vector<std::shared_ptr<foo>> v;
      

      std::vector<std::unique_ptr<foo>> v;
      

      boost::ptr_vector<foo> v; // www.boost.org
      

      以上版本告诉用户如何处理对象的生命周期。使用原始指针可能会导致指针被删除一次或多次,尤其是在代码随时间修改或涉及异常的情况下。

      如果您使用“shared_ptr”或“unique_ptr”之类的接口,这会自行记录用户的生命周期管理。当您使用原始指针时,您必须清楚地记录您如何处理对象的生命周期管理,并希望正确的人在正确的时间阅读文档。

      使用原始指针向量的好处是您可以更灵活地处理生命周期管理,并且您可以摆脱一些性能和空间开销。

      【讨论】:

      • @nbro 这是通常的答案,因为它是正确的答案:)
      • 这不是正确的答案。没有人知道正确的事情。一切都是相对的。
      • 但是+1,因为相对论有时是相似的
      【解决方案6】:

      使用指针向量绝对没有问题。这里大多数都建议使用智能指针,但我只想说,使用没有智能指针的指针向量没有问题。我一直这样做。

      我同意 juanchopanza 的观点,即问题在于您的示例是指针来自 new foo()。在一个正常的完全有效的用例中,您可能将对象放在其他一些集合 C 中,这样当 C 被销毁时,这些对象将自动被销毁。然后,在对 C 中的对象进行深入操作的过程中,您可能会创建任意数量的包含指向 C 中对象的指针的其他集合。(如果其他集合使用对象副本,那将浪费时间和内存,而引用集合是明确禁止的。)在这个用例中,我们永远不想在指针集合被销毁时销毁任何对象。

      【讨论】:

        猜你喜欢
        • 2019-03-22
        • 2016-03-13
        • 2017-08-26
        • 2021-11-26
        • 2010-11-04
        相关资源
        最近更新 更多