为啥下面的代码会崩溃?

Posted

技术标签:

【中文标题】为啥下面的代码会崩溃?【英文标题】:Why does the following code crash?为什么下面的代码会崩溃? 【发布时间】:2012-12-09 17:39:00 【问题描述】:

这只是创建一些列表元素,然后在开始通过反向迭代接近它时删除一个元素。这是一个实际问题的复制品,即在反向遍历元素时删除元素的代码。

#include <list>

int main()

  std::list< int > lst;

  for ( int c = 33; c--; )
    lst.push_back( 0 );

  int count = 0;
  for ( std::list< int >::reverse_iterator i = lst.rbegin(), e = lst.rend();
        i != e; )
  
    switch( count++ )
    
      case 32:
      case 33:
        ++i;
        i = std::list< int >::reverse_iterator( lst.erase( i.base() ) );
      break;
      default:
        ++i;
    
  

  return 0;

运行时会崩溃:

*** glibc detected *** ./a.out: double free or corruption (out): 0x00007fff7f98c230 ***

当使用 valgrind 运行时,它会说:

==11113== Invalid free() / delete / delete[] / realloc()
==11113==    at 0x4C279DC: operator delete(void*) (vg_replace_malloc.c:457)
==11113==    by 0x40104D: __gnu_cxx::new_allocator<std::_List_node<int>     >::deallocate(std::_List_node<int>*, unsigned long) (in /tmp/a.out)
==11113==    by 0x400F47: std::_List_base<int, std::allocator<int>   >::_M_put_node(std::_List_node<int>*) (in /tmp/a.out)
==11113==    by 0x400E50: std::list<int, std::allocator<int> >::_M_erase(std::_List_iterator<int>) (in /tmp/a.out)
==11113==    by 0x400BB6: std::list<int, std::allocator<int> >::erase(std::_List_iterator<int>) (in /tmp/a.out)
==11113==    by 0x40095A: main (in /tmp/a.out)

编译器:

$ g++ --version
g++ (Debian 4.7.1-7) 4.7.1

拱门:

$ uname -a
Linux hostname 3.2.0-2-amd64 #1 SMP Mon Apr 30 05:20:23 UTC 2012 x86_64 GNU/Linux

你认为这是一个错误,还是我在这里做错了什么?

附言如果您删除 case 33(这绝不应该发生),这将变成无限循环而不是崩溃。

【问题讨论】:

count 中是否存在 off by 1 错误? (应该是switch(++count)?) 否 - 循环的退出条件是i != e,它不依赖于计数。 那是 - 它被关闭了,但它不应该导致任何问题。但是,由于内存损坏,迭代实际上继续超过 32。我已经编辑了这个问题,以便更清楚地说明这一点。 您在执行lst.erase(i.base()) 时更新i,但您可能还需要更新e 或简单地使用i != lst.rend() 作为您的for 终止条件(手头没有编译器验证)。 @ChrisSchmich:但为什么e 会失效?它指向第一个元素之前。 【参考方案1】:

好的,所以我拿出笔和纸,现在我认为 与您的 e 迭代器无效有关。请记住,反向迭代器包含一个普通的迭代器,它指向容器中的下一个元素,即它的基迭代器。也就是说,当您拥有指向最后一个元素的 rbegin() 迭代器时,它的内部迭代器将指向最后一个元素。同样,当 rend() 迭代器指向开始前的迭代器(反向迭代器可以指向的虚构元素)时,它的内部迭代器指向第一个元素。

所以你的列表看起来像这样(BTB = 开始之前,PTE = 结束之后):

BTB | 0 | 0 | ... | 0 | 0 | PTE
 ^    :                 ^    :
 |----'                 |----'
 e                      i

虚线表示基础迭代器指向的位置。

现在,在第一次迭代中,您指向最后一个元素(反向第一个),count 为 0,因为您执行后缀递增。因此,当开关与 32 匹配时,您将指向列表中的第一个元素(反向第 33 个)。

好的,现在我们处于这种状态:

BTB | 0 | 0 | ... | 0 | 0 | PTE
 ^    ^   :
 |----|---'
 e    i

然后执行以下代码:

++i;
i = std::list< int >::reverse_iterator( lst.erase( i.base() ) );

第一行让我们处于这种状态:

BTB | 0 | 0 | ... | 0 | 0 | PTE
 ^    :
 |----'
 i
 e

然后你擦除基迭代器指向的元素并设置你的反向迭代器,以便它的基现在指向被擦除元素之后的元素。现在我们有:

    BTB | 0 | ... | 0 | 0 | PTE
 ^   ^    :
 |---|----'
 e   i

不过,现在e 已失效。它的 base 不再指向列表的第一个元素,它指向一个无效元素。

现在,您的循环应该停止,因为 i 已经结束,但它不会。下次再继续,count33,先做i++

    BTB | 0 | ... | 0 | 0 | PTE
 ^   :
 |---'
 i   
 e

然后试图擦除基地。哦亲爱的!基地没有指向一个有效的元素,我们得到了崩溃。事实上,我认为一旦你迭代得太远,你就已经遇到了未定义的行为。

解决方案

修复它的方法是每次迭代时获取rend()

for ( std::list< int >::reverse_iterator i = lst.rbegin();
      i != lst.rend(); )

或者,当您删除元素时更新e

++i;
i = std::list< int >::reverse_iterator( lst.erase( i.base() ) );
e = lst.rend();

现在,我之前的回答是交换增量和擦除,这很有效,但为什么呢?好吧,让我们回到重要的点(为了在接下来的几个步骤中清晰起见,我添加了另一个元素):

BTB | 0 | 0 | 0 | ... | 0 | 0 | PTE
 ^    ^   :
 |----|---'
 e    i

所以现在我们先擦除基础,给我们这个:

BTB | 0 |     0 | ... | 0 | 0 | PTE
 ^    ^       :
 |----|-------'
 e    i

然后我们增加i:

BTB | 0 |     0 | ... | 0 | 0 | PTE
 ^    :
 |----'
 i
 e

然后i == e 结束循环。因此,虽然这 确实 有效,但它并没有达到你想要的效果。它只删除第二个元素。

【讨论】:

我使用了以下广受好评的答案:***.com/questions/8621426/… 嗯,该拿出笔和纸了。 我们查看的是相同的代码吗?我的意思是那里接受的答案。它显然和我做的一样。 @dragonroot 我改变了答案。 @dragonroot 确实如此。 reverse_iterator 是一个迭代器适配器。该标准说:“反向迭代器与其对应的迭代器i 之间的基本关系由身份建立:&amp;*(reverse_iterator(i)) == &amp;*(i - 1).”换句话说,取消引用反向迭代器会在它所适应的迭代器之前为您提供一个元素。 【参考方案2】:

错误是e 失效,你应该直接比较lst.rend()。为什么会失效?好,我们来看看rend()(§23.2.1.9)的定义:reverse_iterator(begin())

所以rend() 的构造依赖于指向第一个元素的begin() 迭代器,您实际上可以用case 32 删除它。因此,由于该操作使begin() 无效,它很可能也使rend() 无效,具体取决于它的实现方式,这显然发生在这个版本的libstdc++ 中。

case 33 让它崩溃也是有道理的,这一次迭代器将指向不再在列表中的东西。删除它当然会无限循环,因为e 无效并且您的停止条件不会命中。

【讨论】:

§23.2.1.9 - 这是来自 ISO/IEC 14882 的吗?我在这里没有看到这样的段落。 这是 n3242 'draft' 版本【参考方案3】:

当你删除元素时,e 无效!缓动后你必须更新e

i = std::list< int >::reverse_iterator( lst.erase( i.base() ) );
e = lst.rend(); // update e

【讨论】:

从列表中擦除只会使您擦除的元素的迭代器无效。我不确定这会对反向迭代器产生什么影响。我想rend 的基迭代器是列表的第一个元素,将失效。 @sftrabbit:感觉确实如此。不过,这对我来说没有多大意义。 因为++i在擦除前,i == e所以那个节点会失效。【参考方案4】:

这应该工作 -

    #include <list>
    #include <iostream>

    int main()
    
    std::list< int > list;
    for ( int c = 33; c--; )
    list.push_back( 0 );

    std::list<int>::reverse_iterator it = list.rbegin();
    int count = 0;
    while(  it != list.rend() )
    

    switch( count++ )
    
     case 32:
     case 33:
     std::cout<<*it<<std::endl;
     it = std::list< int >::reverse_iterator( list.erase((++it).base()));
     std::cout<<list.size()<<std::endl;
     break;
     default:

    ++it;
    
   
  return 0;

【讨论】:

以上是关于为啥下面的代码会崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个简单的 NSWindow 创建代码会在 ARC 下关闭时触发自动释放池崩溃?

为啥 PyQt 在没有信息的情况下会崩溃? (退出代码 0xC0000409)

为啥下面的代码会产生死锁

为啥 ChangeWindowMessageFilter 会导致 Qt 崩溃?

为啥我的代码会崩溃?

NSMutableArray,removeFromArray 和 release,为啥会崩溃?