为什么使用指针向量被认为是不好的?

14

最近我听到了一个观点,认为我不应该使用指针的向量。 我想知道 - 为什么我不能这样做?

例如,如果我有一个类foo,可以这样做:

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

我已经看到一些人对这种做法进行了负面评价,为什么会这样呢?


3
这些对象不会自动销毁。为此,可以使用 Boost 的 ptr_vector 或者 vector<unique_ptr<>> - Columbo
我认为这是因为所有的std容器都会复制它们拥有的所有元素,因此,如果我们在std::vector中有一个指针,并且您对其进行修改,则实际上并没有修改初始指针。 - nbro
3
我想知道为什么我不能做到?你是可以的,但最好使用智能指针。没有上下文的情况下,涉及糟糕实践的问题是毫无意义的。请在问题本身中包含异议意见。 - user3920237
2
问题不在于使用指针的向量。问题是它们指向新对象,而不清楚谁拥有这些对象。 - juanchopanza
1
C++的开发者倾向于使用语言特性强制实施RAII习惯用法。如果你足够了解std::vector和对象作用域,那么就没有问题 - 但是你无法充分利用容器和所有权语义。这也使得异常安全性更难以保证。 - Brett Hale
6个回答

14

使用原始指针的向量并不一定是不好的风格,只要你记住这些指针没有所有权语义。当你开始使用 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。我个人不喜欢这种方法,因为共享指针通常很难理解对象的生命周期。在这个特定的例子中,由于Vertexes之间存在循环引用,共享指针根本无法工作。


14

在容器中存储裸指针可能导致内存泄漏和悬空指针。在容器中存储指针并不定义指针的所有权。因此,容器不知道销毁和复制操作的语义。当容器中的元素被移除时,容器不知道如何正确地销毁它们;当执行复制操作时,没有所有权的语义可用。当然,你总是可以自己处理这些事情,但仍然存在人为错误的可能性。

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

另一个需要提到的是,容器分为非侵入式和侵入式容器 - 它们存储实际提供的对象而不是副本,因此实际上是一组指针。非侵入式指针有一些优点,所以不能一概而论在所有情况下都应该避免在容器中使用指针,但在大多数情况下建议使用智能指针。


我完全不同意这里的所有答案。这应该被避免的主要原因是它增加了额外的间接性并降低了缓存局部性。 - Fabian Keßler

8
因为向量的析构函数不会在指针上调用delete,所以很容易意外泄漏内存。向量的析构函数会调用向量中所有元素的析构函数,但原始指针没有析构函数。
然而,您可以使用智能指针的向量来确保销毁向量将释放其中的对象。vector<unique_ptr<foo>>可在C++11中使用,在C++98中可以使用TR1中的vector<tr1::shared_ptr<foo>>(尽管与原始指针或unique_ptr相比,shared_ptr具有轻微的开销)。
Boost还拥有一个pointer container library,其中特殊的删除-on-destruction行为内置于容器本身中,因此您不需要智能指针。

它不会调用delete,但这取决于程序员,不是吗? (我的意思是在删除向量时需要调用delete) - joppiesaus
3
管理指针的生命周期更加困难,例如,在向量进行复制时。 - user3920237
是的,您可以通过手动删除所有元素来避免内存泄漏,但这可能很棘手(例如当涉及异常时),而且很容易忘记或出错。并不是说原始指针容器具体不被鼓励使用,而是一般情况下都不鼓励使用原始指针。除非您有一个非常好的理由,否则请使用智能指针。 - Wyzard

5

其中一个问题是异常安全性(exception-safety)

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

相反,如果您有一个智能指针向量(例如std::vector<std::unique_ptr<Foo>>),则如果调用向量的析构函数,则向量中每个智能指针安全拥有的指向物(由智能指针安全拥有)将被正确删除,从而调用其析构函数。因此,与每个项相关联的资源(在向量中以“聪明”的方式指向)将得到适当释放。

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


3
我将具体讨论一个指针向量,它负责管理所指对象的生命周期,因为只有在这种情况下,指针向量才是明显可疑的选择。
有更好的替代方案。具体来说:
std::vector<std::shared_ptr<foo>> v;

and

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

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

上述版本告诉用户如何处理对象的生命周期。如果使用原始指针,可能会导致指针被删除的次数多或少,特别是如果代码随着时间的推移而修改,或者涉及到异常。
如果使用像´shared_ptr´或´unique_ptr´这样的接口,这将为用户记录生命周期管理。当您使用原始指针时,必须清楚地记录如何处理对象的生命周期,并希望正确的人在正确的时间阅读文档。
使用原始指针的好处是可以更灵活地处理生命周期管理,并且可以可能消除一些性能和空间开销。

@nbro 这是通常的答案,因为这是正确的答案 :) - Peter
这不是正确的答案。没有人知道正确的事情。一切都是相对的。 - nbro
但是加1,因为相对论有时很相似。 - nbro

1

使用指针的向量没有任何问题。大多数人建议使用智能指针,但我必须说,使用指针的向量没有问题。我经常这样做。

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


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