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

Why is using vector of pointers considered bad?

本文关键字:认为是 向量 指针 为什么      更新时间:2023-10-16

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

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

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

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

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

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

另一件值得一提的事情是,容器分为非侵入式和侵入式连接器 - 它们存储实际提供的对象而不是副本,因此它实际上归结为指针集合。非侵入式指针具有一些优点,因此您不能概括容器中的指针是始终应避免使用的东西,但在大多数情况下仍建议使用。

使用原始指针向量不一定是糟糕的风格,只要您记住指针没有所有权语义。当您开始使用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之间的循环引用,共享指针根本不起作用。

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

但是,您可以使用智能指针向量来确保销毁向量将释放其中的对象。 vector<unique_ptr<foo>>可以在 C++11 中使用,在 C++98 中使用 TR1 您可以使用vector<tr1::shared_ptr<foo>>(尽管与原始指针或unique_ptr相比shared_ptr开销略有。

Boost 还有一个指针容器库,其中特殊的销毁时删除行为内置于容器本身中,因此您不需要智能指针。

其中一个问题是异常安全

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

相反,如果你有一个智能指针的向量(例如 std::vector<std::unique_ptr<Foo>> (,则如果调用了向量析构函数,则正确删除向量中的每个指向项(由智能指针安全拥有(,并调用其析构函数。因此,与每个项目关联的资源(在向量中"智能"指向(被正确释放。

请注意,观察原始指针的向量是正常的(假设观察项的生存期超过向量的生存期(。问题在于原始拥有指针。

我将专门讨论负责管理指向对象的生存期的指针向量,因为这是指针向量显然是一个可疑选择的唯一情况。

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

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"之类的界面,则会自行记录用户的生命周期管理。当你使用原始指针时,你必须清楚地记录你如何处理对象的生存期管理,并希望正确的人在正确的时间阅读文档。

使用原始指针向量的好处是,在如何处理生存期管理方面具有更大的灵活性,并且可能会摆脱一些性能和空间开销。

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

我同意 juanchopanza 的观点,问题是你的例子是指针来自新的 foo((。在正常的完全有效的用例中,您可能将对象放在其他集合 C 中,以便在销毁 C 时自动销毁这些对象。然后,在对 C 中的对象执行深入操作的过程中,您可以创建任意数量的其他集合,其中包含指向 C 中的对象的指针。 (如果其他集合使用对象副本,那将浪费时间和内存,而引用集合是明确禁止的。在此用例中,我们永远不希望在销毁指针集合时销毁任何对象。