【问题标题】:Does insertion of elements in a vector damages a pointer to the vector?在向量中插入元素会损坏指向向量的指针吗?
【发布时间】:2011-03-30 03:35:20
【问题描述】:

在一个模拟逻辑门的程序中,我从使用数组切换

node N[1000];

到向量

vector<node> N;

而且我的程序在使用向量之前确实运行良好,但现在打印错误的结果,所以我尝试调试,发现错误发生在这里:

node* Simulator::FindNode(string h)
{
    int i;
    for(i = 0; i < NNodes; i++)
    {
        if (N[i].getname() == h)
        {
            return &N[i];
        }
    }

    node n ;
    N.push_back(n);
    N[NNodes].setname(h);
    NNodes++;
    return &N[NNodes-1]; //why?because of NNodes++  
}

// ...

node* inp1;
node* inp2;
node* out;
string NodeName;

inp_file >> NodeName;
inp1 = FindNode(NodeName);
s1 = inp1;

inp_file >> NodeName;
inp2 = FindNode(NodeName); //inp1 is destroyed here 

inp_file >> NodeName;
out = FindNode(NodeName); //inp2 and inp1 are destroyed here 

第一次调用FindNode 时,第一个指针inp1 指向正确的位置,即&amp;N[0]

第二次调用FindNode时,第一个指针inp1指向垃圾,第二个指针inp2指向正确的地方&amp;N[1]

当第三次调用FindNode 时,第一个和第二个指针(inp1inp2)都指向垃圾!第三个指针指向正确的位置。

为什么会发生这种情况?
当我向它们插入项时,vector 是如何工作的,我应该使用哪种指针来指向 vector 项?

【问题讨论】:

  • 我格式化了你的帖子。我还更改了您的代码,在事物之间放置空格。执行:for(i = 0; i &lt; NNodes; i++) 而不是 for(i=0;i&lt;NNodes;i++) 或执行 inp_file &gt;&gt; NodeName; 而不是 inp_file&gt;&gt;NodeName;,这样可读性更强。
  • 我想建议您将 GMan 的答案标记为已接受。这是这里最全面的。
  • 感谢大家特别是 GMan 和 Steven Sudit 的回答,现在我要学习和修改的太多了。

标签: c++ pointers vector


【解决方案1】:

是的,它可以重新分配整个缓冲区,使所有指向旧位置的指针无效。

您可以通过预分配来限制这一点,但这实际上只是提高了性能。更好的方法是使用索引而不是原始指针。

【讨论】:

  • node*替换为int,并存储索引。当您需要一个节点时,您必须使用该索引在向量中查找它。
  • 我不太明白。你能解释一下代码示例吗?
  • 如果你必须保持指针语义,你可以使用 std::list 代替:sgi.com/tech/stl/List.html 你使用迭代器而不是指针进入这个列表(std::list::iterator)。此列表中的迭代器的行为类似于指针,并且在插入或删除元素时它们保持其值。 std::list 消耗更多内存,没有随机访问“N[i]”。
  • 这很有趣:我似乎无缘无故被否决了。一定很喜欢。
  • 无论如何,我认为 GMan 的字典建议可能更合适。
【解决方案2】:

一些事情。

首先,据我所知NNodes 只是在跟踪大小。但是你有std::vector::size()。然后,您可以使用它来获取最后插入的元素,但您可以使用 std::vector::back()return &amp;N.back();

您的参数也是按值传递的,而它可能应该通过 const-reference 传递:const string&amp; h。这样可以避免不必要的复制,并且通常*您应该通过 const 引用而不是按值传递内容。

这很糟糕:

node n;
N.push_back(n);
N[NNodes].setname(h);

node 应该有一个构造函数,它接受const string&amp; 并在初始化期间设置名称。这样你就永远不会有一个没有名字的节点,例如:

node n(h);
N.push_back(n);

或更简洁:

N.push_back(node(h));

好多了。

第二,是的,vector 可以使指向元素的指针无效;即,每当需要增加向量的容量时。如果可以,reserve() 预先设置容量以避免重新分配。在你的情况下你不能,所以你可以走两条不同的路线。

第一条路线是a level of indirection。与其直接指向事物,不如将它们的索引放入数组中。请注意,虽然它们的地址可能会改变,但它们在向量中的位置不会改变。你会让Simulator::FindNode 返回size_t,然后返回N.size() - 1。添加一个像node&amp; GetNode(size_t index) 这样的成员,它只是做return N[index];(如果你愿意,会检查错误)。现在,当您需要一个成员时,将该成员的索引交给GetNode,您将获得对该节点的引用。

另一条路线是更改您的容器。例如,您可以使用deque。这没有连续存储,但很像vectorpush_backpop_back 仍然是 O(1),并且它仍然具有良好的缓存一致性。 (顺便说一句,deque 也在 O(1) 时间内用连续存储换取push_frontpop_front 的能力)

重要的是deque不会在来自任一端的推送或弹出操作期间使指针无效。它通过一种矢量列表混合方式工作,您可以在其中获得链接在一起的元素的大块存储。把你的底层存储改成deque(中间不要拿或放任何东西),你就可以指向东西就好了。

但是,据我所知,您的地图效率非常低。您正在将名称映射到节点。您可能应该只使用std::map,它具有您尝试重新创建的确切界面。您甚至可以指向地图中的任何元素,这绝不会使事物无效。

*规则是,通过 const-reference 传递,除非类型是原始类型(内置,如 intdouble 等),如果类型大小小于 sizeof(void*),或者如果您将需要无论如何都要复制一份。

也就是说,不要这样做:

void foo(const std::string& s)
{
    std::string ss(s); // make a copy, use copy
}

但是这样做:

void foo(std::string s) // make a copy, use copy
{
}

【讨论】:

  • 经过一些试验,我得出结论,如果您需要在程序中使用指针,则使用某些间接的第一条路线将无法正常工作。出现了很多问题,解决它们必须有不必要的复杂性,所以我使用了一个双端队列,它就像魅力一样工作。
【解决方案3】:

当向量增长时,它会被重新分配,这实际上会使指向向量元素的所有指针无效。

如果您事先知道向量中有多少个元素,您可以使用reserve() 方法预分配空间。

【讨论】:

  • 我没有,这取决于用户输入。
  • Peter,我提到了预分配,但我不建议将其作为指针失效的解决方案,仅作为性能优化。如果您必须提前调整大小,使用List&lt;&gt; 有什么意义?那么还不如只使用原生数组。
  • 我知道,但是向量提供的好处不仅仅是“更好的数组”。无论如何我同意你的观点,你的解决方案更好。
  • 彼得,你没看错。如果我们知道有一些合理的最大值,我们可以保留完整大小,同时仍然具有具有有效当前长度的可扩展向量的语义。唯一的问题是 OP 说没有这样的最大值。
【解决方案4】:

返回一个指向内部 STL 成员的指针可能不是最好的主意。当您将对象提供给 STL 容器时,您基本上放弃了对它的控制。你告诉 STL 它可以在它认为合适的时候移动它,以维持容器给你的承诺。返回节点所在的索引是一个更好的主意,就像 Steven Sudit 提到的那样。

获得索引后,您可以创建一个函数,该函数返回您感兴趣的节点内容的副本。这样您还可以使用 STL 容器维护数据封装,不允许其他任何人修改其内容。

【讨论】:

  • 当然可以,但是如果指针在停止更改容器后被取出就好了:STL 保证这是安全的。
  • 而且,正如其他一些人所指出的,不使用大的连续块的容器确实保证地址不会改变。
【解决方案5】:

是的,插入将使指向向量元素的旧指针在重新分配时失效。如果你真的很想使用稳定的指针,你可以从向量切换到双端队列。它提供了一个与向量非常相似的接口,可以在不重新分配的情况下增长,并通过分配更多的块来移动以前的内容。

使用双端队列而不是向量所付出的代价是在随机访问上多了一层间接性。根据您的使用情况,这可能完全无关紧要。如有必要,您应该使用迭代器迭代整个双端队列。这将与迭代向量一样快。

收益是:零重新分配!

【讨论】:

    【解决方案6】:

    假设您需要编写基于数组的代码,以便在必要时重新调整数组的大小。

    想象一下你会怎么做。

    想象一下过时的指针会发生什么。

    重写您的代码以使用索引而不是指针,或确保不会发生重新分配,视情况而定。

    【讨论】:

      猜你喜欢
      • 2022-01-21
      • 2011-03-12
      • 1970-01-01
      • 2011-08-04
      • 2015-12-06
      • 1970-01-01
      • 2018-12-16
      • 2010-10-13
      • 1970-01-01
      相关资源
      最近更新 更多