【问题标题】:What happens to a pointer that points to an element in a vector when I shuffle it?当我洗牌时,指向向量中元素的指针会发生什么?
【发布时间】:2015-04-25 09:55:30
【问题描述】:

我有一个std::vector<int> 和一个指针int*,它们指向向量中的一个元素。假设指针指向第三个元素:pointer=&vector.at(2)。如果我现在对向量进行洗牌,它是否仍会指向同一个元素(第三个),还是会指向曾经是第三个元素现在已移动的新位置?

在那之后,我想让这个问题更笼统一点:当向量扩展或缩减时,指向向量中元素的指针和迭代器的行为如何?

【问题讨论】:

  • 一个指针和地址,一个内存位置。它指向其地址中的任何内容;除非指针本身改变,否则它总是指向同一个地址。
  • 花点时间想想什么是指针?指针是指向内存的地址。不管你如何洗牌向量,指针仍然指向同一个地址。
  • 它们保持不变,这就是问题所在。
  • 您不能在 C++ 中移动对象。 (你可以有移动构造函数等,但在这个问题的上下文中,这些不是“移动对象”的意思)

标签: c++ pointers vector


【解决方案1】:

如果向量在没有调整大小的情况下被打乱,那么指针仍然指向相同的位置,这可能包含不同的元素。

如果将向量的大小调整为更大,则该指针被称为“无效”并且它具有与未初始化指针相同的状态,即评估它或尝试读取它会导致未定义的行为。

【讨论】:

  • 你怎么能说指针将可能指向不同的元素,而不知道它的内容?想象一个只有 3 的向量...
  • @Theolodis 对我来说似乎不太可能有人会洗牌只包含 3 的向量
【解决方案2】:

指针将继续指向相同的位置,因此当您随机播放时,它会指向已移动到您指定位置的任何元素。

当您扩展向量的大小时,所有现有的指向向量的指针和迭代器都可能变得无效。当您洗牌时,它们会继续引用同一个位置,该位置(通常)包含与洗牌前不同的值。

减小向量的大小将取决于您如何做到这一点。一种方法是创建一个临时向量作为当前向量的副本,交换两者,然后销毁临时向量(通常是隐式地,通过让它超出范围)。如果这样做,指针将进入临时对象,并在销毁时失效。

如果您使用shrink_to_fit,那(可能)不会使迭代器/指针无效,但可能不会产生任何影响(标准指定它是一个非绑定请求,并且没有说明它会使迭代器/指针)。

【讨论】:

  • “所有现有的指向向量的指针和迭代器都变得无效”可能是真的,但可能更正确,因为“现有的指针和迭代器不能保证保持有效”。
  • @thang 这些意思是一样的。
  • 在不使指针和迭代器失效的情况下,您实际上如何shrink_to_fit
  • @T.C.:[一些] 内存管理器可以就地拆分分配的块。其他人可能会忽略该请求——但无论哪种方式,该标准似乎都没有允许 shrink_to_fit 使迭代器无效。
  • @JerryCoffin 请参阅LWG 2223 关于shrink_to_fit 与迭代器失效问题。
【解决方案3】:

如果我现在对向量进行洗牌,它是否仍会指向同一个元素(第三个),还是会指向已使用到第三个元素已移动的新位置?

混洗元素只是通过数组中的各种“桶”复制/交换元素的问题,而您的指针只是指向“内存中的那个固定位置”。所以,它会一直指向数组中的第三个位置。

然后我想把这个问题说得更笼统一些:当向量被扩展、缩小或洗牌时,指向向量中元素的指针和迭代器的行为如何?

扩展:所有迭代器/引用/指针都可能失效。

减少:只要它们指向删除之前的元素,它们将保持有效除非您执行shrink_to_fit。指向您删除的元素的迭代器/指针显然无效。

Shuffled:你在移动东西而不导致重新分配,所以迭代器和引用仍然有效。

请注意,大多数 C++ 文档源通常都会报告所有这些内容。


对于向量要记住的概念规则是,它们只是动态数组周围的一个盒子,迭代器和指向元素的指针在概念上是相同的(实际上,std::vector<T>::iterator 可能是 typedefT *) .引用(伪装的指针)也是如此。

如果一个操作可能需要重新分配数组(=数组需要增长,或者您明确要求它缩小),那么所有迭代器/指针/引用都将失效。如果删除元素,则指向向量“概念端”的指针将指向无效元素。如果大小保持不变,则无需进行重新分配。

【讨论】:

    【解决方案4】:

    地址不会改变,但存储在该地址的值会改变。

    #include <iostream>
    #include <algorithm>
    
    static void print_vec(const std::vector<int>& v) {
        for (int i = 0; i < v.size();  ++i) {
            std::cout << "Value: " << v.at(i) << " Address: " << &v.at(i) << std::endl;
        }
    }
    
    static void shuffle_vec(std::vector<int>& v) {
        auto engine = std::default_random_engine{};
        std::shuffle(v.begin(), v.end(), engine);
    }
    
    int main() {
        std::vector<int> v{1, 2, 3, 4, 5};
        std::cout << "Before Shuffle: " << std::endl;
        print_vec(v);
        shuffle_vec(v);
        std::cout << "After Shuffle: " << std::endl;
        print_vec(v);
    
        return 0;
    }
    

    输出:

    Before Shuffle: 
    Value: 1 Address: 0x16eb320
    Value: 2 Address: 0x16eb324
    Value: 3 Address: 0x16eb328
    Value: 4 Address: 0x16eb32c
    Value: 5 Address: 0x16eb330
    After Shuffle: 
    Value: 3 Address: 0x16eb320
    Value: 1 Address: 0x16eb324
    Value: 5 Address: 0x16eb328
    Value: 4 Address: 0x16eb32c
    Value: 2 Address: 0x16eb330
    

    【讨论】:

      【解决方案5】:

      实际上,向量是由代码维护的连续数据缓冲区。每个元素都以类似数组的方式与下一个元素相邻设置。

      当元素四处移动时,实际上,它们只是四处移动。指针指向该缓冲区中的位置,因此如果元素移动,实际上指针最终会指向其他位置。

      但是,C++ 标准更加严格。当迭代器失效时,指向该位置的引用和指针也会失效。有许多操作可以使迭代器无效,从逻辑上讲,这些操作不会改变数组实际上将是同一个缓冲区的事实。例如,如果您.erase 一个元素,它将使该位置及之后的每个迭代器无效。

      实际上,指向元素的指针最终会指向列表中的“下一个”元素,但标准并不能保证这一点。

      std::shuffle 不会使任何迭代器失效。它只是更改存储在那里的值。因此,指向第 n 个元素的指针在实践中和理论上仍然指向第 n 个元素。

      当向量展开时,如果将其展开超出.capacity(),则所有迭代器都将失效。在实践中,它实际上将数据移动到一个新位置,并且指针现在是悬空指针。

      当您减少(通过.erase(it).erase(a,b))时,第一个参数处或之后的所有迭代器都将失效。这意味着对这些元素的引用和指针也无效。在实践中,它们现在将引用“更下游”的元素(如果存在此类元素),因为.erase 都不会导致您的缓冲区重新分配,但标准不保证这一点。

      还有其他操作可以使迭代器无效。 .shrink_to_fit() 可以,vector&lt;X&gt;(vec).swap(vec)(C++03 版本的收缩适应)和 .reserve() 以及将大小增加到超过 .capacity() 的操作也可以。

      在实践和理论上,导致.capcity() 更改的操作实际上会使您的指针无效(或使指针指向超出末端的那些)。

      【讨论】:

      • 为什么要多次说“in practice”? (理论上也是这样)
      • @matt vec.erase(vec.begin()) 理论上会使指向向量任何元素的每个指针无效。在实践中,它们仍然指向元素,但标准没有谈论“现在指向下一个元素”的半无效。我想不出一个会破坏它的实现,除了用病态语言实现实际标记指针无效。
      • 不符合标准的代码具有未定义的行为。提出未定义行为的典型实现具有误导性且无关紧要。 (对您而言,这样做的是什么?)
      • @philipxy 我能想到 3 个很好的理由,还有 1 个平庸的理由。首先,认识到你必须小心什么样的错误,因为测试不会发现它:知道预期的行为(即使未定义)会告诉你很多。其次,我可能是错的:虽然引用和迭代器可能无效,但标准中的连续性保证和指针算术保证可能会使指针比我想象的更明确!第三,当您遇到每个实现都必须以某种方式运行的情况时,未来的标准可能会强制要求它。第四,有时你只能作弊。
      【解决方案6】:

      正如人们所提到的,指针“指向”内存中的某个位置,而不管那里的内容如何。实际上,您可以做一些非常有趣的事情,例如拥有一个由 5 个元素组成的数组,但打印出位置 6 的值,即使它不在您的数组范围内。当您仅将数组声明为 5 个元素长时,通过使用类似数组 [5] 的东西访问数组,您最终会出现未定义的行为,本质上意味着可能会发生各种事情,每次运行都可能返回完全不同的东西。请参阅下面的 philipxy 的 cmets,了解一些深入研究此概念的非常有用的链接。

      因此,除此之外,这里有一些代码,您可以测试以实际看到这种效果。

      #include <vector>
      #include <iostream>
      
      using namespace std;
      
      int main()
      {
          vector<int> values (5);
      
          for (int i = 0; i < 5; i++)
              values[i] = i;
      
          for (int i = 0; i < 5; i++)
              cout << values[i] << " ";
      
          //Initialise the pointer so that it is pointing at the first element in vector
          int* pointer = &values[0];
      
          //By incrementing, we expect it to be pointing at the second element, which should be 1
          pointer++;
      
          cout << endl << "Pointer " << *pointer << endl;
      
          //Reverse the order of the vector
          reverse(values.begin(), values.end());
      
          for (int i = 0; i < 5; i++)
              cout << values[i] << " ";
      
          cout << endl << "Pointer " << *pointer << endl; 
      
          return 0;
      }
      

      这段代码的结果是:

      所以我们可以看到指针实际上并没有改变它指向的位置,但是内存中的那个单元已经改变了,所以取消引用指针会产生不同的结果。

      【讨论】:

      • 访问超出其大小的数组或向量是未定义的行为。你不能正确地说它会做任何特别的事情。
      • @Philipxy 是的,我认为这是一个合理的评论。我的意思是,尝试超出数组的定义区域不一定会停止,它会很高兴地尝试使用单元格,无论成功与否。但是,如果该陈述不正确,您能否指出不是这种情况的情况?我总是有兴趣了解更多信息,然后我会相应地更新答案。
      • C/C++ (etc) 标准定义了程序做什么,有些有undefined behavior。除了指定的内容之外,人们根本无法期待任何事情。阅读as-if rule。或specificationsemanticsprogramming languages。请参阅我之前的评论:对你来说这样做的point是什么?
      • @philipxy 我已阅读完您提供的未定义行为链接的第一页,非常有兴趣继续阅读其他两页。 as-if 规则也是一个非常有趣的读物,尽管在不一定需要应用它的情况下(使用复制/移动)似乎有些有趣。非常感谢您链接到此信息,这确实是一本非常有趣的书,尽管其中一些概念对我来说有点晦涩难懂。我现在将编辑我的答案以反映我所学到的内容。
      【解决方案7】:

      这完全取决于“std::vector”是如何实现的。我不确定这方面是否有任何保证。

      编辑:我刚刚了解到,C++ 标准确实比我想象的要严格得多(感谢 philipxy)。它确实指定向量必须在内部表现得像 C 数组。见

      http://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/

      所以忘记其余的,至少如果你有一个至少符合 C++03 的实现。

      例如,如果您将 std::vector 实现为链表(不太可能),则改组、减小大小等将无济于事。

      如果“std::vector”在内部使用“int []”之类的东西来存储它的元素(很可能),那么重新洗牌可能意味着你的指针现在将指向一个与以前不同的值(什么 Stevo试过了)。
      如果在这种情况下调整向量的大小,那么它完全取决于内部实现。调整大小可能分配一个新的“int []”并复制旧内容。在这种情况下,您的指针将指向现在未分配的内存,因此所有破坏都可能会破裂。
      如果你很幸运(取决于实现),那么将向量缩小或增长“小”量可能不会做任何事情(你的指针仍然有效)。

      总结:不要那样做 ;-)(使用指针然后修改你的容器)。

      【讨论】:

      • 不,所有这些行为都是指定的。 (见我的回答。)
      【解决方案8】:

      阅读您调用的每个函数的文档。如果您不知道何时以及如何调用它以及它的作用,那么您为什么要使用它?

      一般来说,您不能依赖地址或数组等实现概念,也不能依赖测试程序。对于特定容器、迭代器和运算符的哪些元素,您必须阅读迭代器何时失效或不失效。

      vector::shrink_to_fit 使所有迭代器失效
      vector::resize 相同或更小会使超出新大小的迭代器完全失效
      vector::resize to large 使所有迭代器无效

      来自 C++14 标准 [iterator.requirements.general]:

      [P] 指针是迭代器。取消引用迭代器的效果 已失效未定义。

      http://en.cppreference.com/w/cpp/container/vector

      std::vector 是一个封装动态大小数组的序列容器。
      元素是连续存储的,这意味着元素可以 不仅通过迭代器访问,而且还使用常规的偏移量 指向元素的指针。

      迭代器     RandomAccessIterator

      迭代器失效
      swap, std::swap    从不
      shrink_to_fit    总是
      resize    如果向量改变了容量,所有这些。 如果不是,则只有插入点之后的那些。

      http://en.cppreference.com/w/cpp/container/vector/resize

      当调整到更小的尺寸时,矢量容量永远不会减少,因为 这将使所有迭代器无效,而不仅仅是那些 将被等效的pop_back() 调用序列无效。

      vector::shuffle 之后的迭代器/指针不变,但取消引用新值。

      因为shuffle 使用swap 使迭代器保持不变:

      http://en.cppreference.com/w/cpp/algorithm/random_shuffle

      template< class RandomIt, class URNG >
      void shuffle( RandomIt first, RandomIt last, URNG&& g );
      

      Random 必须满足 ValueSwappable 和 RandomAccessIterator。

      http://en.cppreference.com/w/cpp/concept/Iterator

      迭代器是其他迭代器类型使用的基本概念: InputIterator、OutputIterator、ForwardIterator、BidirectionalIterator、 和随机访问迭代器。迭代器可以被认为是 指针的抽象。 [...]

      `- 满足 Swappable 类型的左值 [...]

      http://en.cppreference.com/w/cpp/concept/ValueSwappable

      类型 T 是 ValueSwappable if
      1) 类型 T 满足迭代器要求
      2) 对于任何 T 类型的可解引用对象 x(即,任何其他值 比结束迭代器),*x 满足 Swappable 要求。

      http://en.cppreference.com/w/cpp/concept/Swappable

      using std::swap;
      swap(u, t);
      

      调用后,t的值就是调用前u持有的值, 而 u 的值就是调用前 t 持有的值。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-12
        • 1970-01-01
        • 2013-01-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多