【问题标题】:C++: vector of pointer loses the reference after push_back()C++:指针向量在 push_back() 之后失去引用
【发布时间】:2016-04-14 23:22:52
【问题描述】:

在我的代码中,a 有一个 Node 对象的全局向量和一个 Node 指针的局部向量:

#include<cstdio>
#include<cstdlib>
#include<vector>

using namespace std;

class Node {
    int n;

public:
    Node(int i) : n(i);
    int getN() { return n; }
};

vector<Node> v;

int main() {
    vector<Node*> p;
    v.push_back(Node(1));
    p.push_back(&v[0]);
    printf("first node id : %d\n", (*p[0]).getN());

    return 0;
}

我将一个节点对象插入到全局向量中,并将该对象的指针插入到本地向量中。我上面代码的输出是:

first node id : 1

但是,如果我将主要功能更改为:

int main()
{
    vector<Node*> p;
    v.push_back(Node(1));
    p.push_back(&v[0]);
    v.push_back(Node(2));
    p.push_back(&v[1]);
    printf("first node id : %d\n", (*p[0]).getN());

    return 0;
}

代码打印一个垃圾值:

first node id : 32390176

我无法找出问题所在。 vector 数据结构是否会在插入后更改每个对象的引用? 我该如何解决这个问题?

【问题讨论】:

  • 让你的代码更简洁一些(并在 ctor 中使用初始化列表)。
  • 我很确定我们有一个副本。无论如何,查阅参考文档(参见Iterator invalidation)应该已经足够清楚了。
  • @πάνταῥεῖ 一个很好的起点。但是,虽然引用失效意味着迭代器失效,但反过来并不成立。
  • @palatok:你知道,你可以用普通的ints 而不是Nodes 包装ints,这将是一样的......跨度>
  • @πάνταῥεῖ:许多新手程序员在开始使用 C++ 向量时并不知道迭代器是什么,并且可能不会意识到“迭代器无效”之类的东西与他们正在做的事情有关。跨度>

标签: c++ pointers vector


【解决方案1】:

“向量在插入后会改变引用吗?”

可能,是的。当您添加/push_back() 附加元素时,std::vector 可能会重新分配其(堆)存储空间,从而使所有指针无效:

迭代器 [阅读:指针] 失效

(用于操作)push_backemplace_back ...如果向量改变了容量,所有这些[即所有迭代器都无效]。如果没有,只有end()

“我该如何解决这个问题?”

如果向量的容量由于插入而没有改变,则上述无效规则不适用 - 因为向量不会不必要地重新分配存储空间。因此,如果您在示例中将向量的容量预设为 2(例如,使用v.reserve(2)),则指针将保持有效。如果事先不知道大小,但可以延迟构造第二个向量(使用指针),则不必保留,插入最后一个元素后即可获得大小。

但是,上述方法非常不推荐。如果您要使您的向量 constant - 至少在您将构造和使用第二个向量的函数范围内 - 您将有一个不重新分配的强有力保证。或者,如果您可以提前确定大小,则可以使用std::array,并且使用指向该容器存储的指针会更合适:

迭代器失效

通常,数组的迭代器在数组的整个生命周期内都不会失效。

您也可以考虑将 indices 存储到您的向量中(尽管在那里,向量也可能会缩小,使索引无效,或者您可能会在中间插入元素等)。

无论如何,我怀疑您可能实际上并不想这样做,也就是说,它似乎不是一个可以用完全不同的方法处理的问题的不太好的解决方案。

PS - 如果向量有custom allocator,那么我写的所有内容都可能无关紧要。

【讨论】:

    【解决方案2】:

    是的,如果必须重新分配,向量上的push_back() 会使对该向量中元素的所有引用(和指针)无效。有多种方法可以解决此问题。如果您知道您的向量将具有特定数量的节点,您可以使用reserve()。在您的示例中,您可以保留两个元素:

    int main()
    {
        v.reserve(2);
        .
        .
        .
    }
    

    这将确保向量已经预先分配了足够的存储空间,因此它不需要重新分配。

    如果您不提前知道大小,那么您将不得不改变您的方法。您可以使用std::deque 而不是std::vector,因为当您使用push_back()std::deque 不会使引用无效。您可以存储索引而不是指针。或者您可能需要在创建指针之前将所有节点推入向量中。

    int main()
    {
        v.push_back(Node(1));
        v.push_back(Node(2));
    
        vector<Node*> p;
        p.push_back(&v[0]);
        p.push_back(&v[1]);
    
        printf("first node id : %d\n", (*p[0]).getN());
    
        return 0;
    }
    

    【讨论】:

    • 谢谢,我现在可以这样做。但是如果我不知道尺寸怎么办?这种情况有什么解决办法吗?在这段代码中使用向量的原因之一是我不知道大小:(
    • @palatok:我已经扩展了我的答案。
    • 谢谢,在收集指针之前插入所有节点似乎是一个很好的解决方法。非常感谢!!!
    【解决方案3】:

    您所做的是向量 p 的未定义行为,因为向量 v 可以更改其对象的存储位置。

    std::vector 的内存是连续的,因此它可能在经过多个push_backs 之后,必须分配一个新的块内存并将其内容复制到新块中。这将使所有碰巧指向旧内存位置的指针失效。

    【讨论】:

    • 我该如何解决这个问题?使用 vector 是否有解决我的问题的方法?我必须使用向量和指针
    • 如果你必须使用std::vector,你可以预先reserve你所有必要的空间(但你必须知道你的向量的大小是多少)。这将防止重新分配。
    • @palatok:“解决这个问题”是什么意思?您不能更改标准库。你想达到什么目的?您有什么要求,您愿意更改设计的哪些部分?
    • 对不起,我不是说要在标准库中修复。我的意思是如何使用相同的数据结构修复我的代码。无论如何,现在我有了解决问题的想法,谢谢
    【解决方案4】:

    您偶然发现了 C++ 的著名“黑暗角落”之一:伟大的“迭代器失效”。一定要熟悉这个:

    Iterator invalidation rules

    特别是,您正在触及这个现实:

    vector:插入点之前的所有迭代器和引用都是 不受影响,除非新的容器大小大于之前的 容量(在这种情况下,所有迭代器和引用都无效) [23.2.4.3/1]

    (重点是我的)

    现在,关于您的问题。您可以确保向量永远不会重新分配。或者,您可以使用没有此问题的其他容器。根据您的需要,所有容器类型之间都有折衷。彻底检查其他问题并做出明智的决定。

    【讨论】:

    • 这不是一个“黑暗角落”。您无法自行管理的指向内存的指针本质上是不可信的......
    • @einpoklum 好吧,“暗角”充其量只是一个模糊和主观的术语,但我在过去几个月中看到了很多关于 HN 的帖子,专门将迭代器失效作为一个重要的“暗角”语言,所以我猜这是一种流行的情绪。但你可以随心所欲地称呼它,这是一个愚蠢的术语。
    • 我实际上不知道 HN 是什么意思,但是 - 好吧,很公平......事情在幕后发生了变化。
    猜你喜欢
    • 1970-01-01
    • 2014-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-09
    • 2021-06-18
    • 1970-01-01
    相关资源
    最近更新 更多