C++容器:在遍歷過程中刪除元素


C++11之后,標准庫引入了大量由基本數據結構封裝而成的容器類型。容器的引入,一定程度上降低Cpp的上手難度。

在實際的開發過程中,經常需要根據業務需求,在遍歷過程中從容器里刪除指定的元素。而一些不規范的使用方式,將埋下穩定性風險。

 

一、推薦模板

對於在遍歷過程中刪除指定元素,推薦使用以下模板:

for(auto it = _container.begin(); it != _container.end(); ) {
    if(__pred(*it)) {
        //do something with (*it);
        it = _container.erase(it);
    } else {
        ++it;
    }
}

  

二、常見遍歷模式的缺陷

1. 下面這種遍歷方式應該是大家最容易想到的:

for(auto it = _container.begin(); it != _container.end(); ++it) {
    if(__pred(*it)) {
        _container.erase(it);
    }
}

這種遍歷方式的問題比較明顯,主要是兩種:

  • 間隔遍歷,部分元素被跳過;
  • heap-buffer-used-after-free,也就是使用被釋放的動態內存;

第一類問題主要出現在以vector為代表的容器中

int main() {
    std::vector<int> nums{1,2,3,4,5,6,7,8};
    for(auto iter = nums.begin(); iter != nums.end(); ++iter) {
        if(*iter == 4) {
            nums.erase(iter);
        } else {
            std::cout<< *iter <<" ";
        }
    }
    std::cout<<std::endl;
    return 0;
}

  g++ _Container.cpp -o _Container && _Container

      

      可以看到刪除元素4之后,其之后的5也被循環跳過了。其原因在於,vector在erase元素時,會將之后的數據往前移動。也就是說,在未++iter之前,iter已經指向下一個元素了。這一點可以從vector的erase實現佐證:

erase(const_iterator __position)
{ return _M_erase(begin() + (__position - cbegin())); }

template<typename _Tp, typename _Alloc>
    typename vector<_Tp, _Alloc>::iterator
    vector<_Tp, _Alloc>::
    _M_erase(iterator __position)
    {
      if (__position + 1 != end())
	_GLIBCXX_MOVE3(__position + 1, end(), __position);
      --this->_M_impl._M_finish;
      _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
      _GLIBCXX_ASAN_ANNOTATE_SHRINK(1);
      return __position;
    }

  _GLIBCXX_MOVE3 將當前iter之后的元素向前挪動了一個元素。

 

第二類問題主要出現在以list、map為代表的容器中

int main() {
    std::unordered_map<int, int> nums{{1,2}, {2,3}, {3,4}};
    for(auto iter = nums.begin(); iter != nums.end(); ++iter) {
        if(iter->first == 2) {
            nums.erase(iter);
        }
    }
    return 0;
}

g++ _Container.cpp -o _Container && _Container

 程序崩潰后報 heap-use-after-free錯誤,並輸出崩潰時的調用棧。我們試着從stl的源碼尋找錯誤的原因。

template<typename _Key, typename _Value,
	   typename _Alloc, typename _ExtractKey, typename _Equal,
	   typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
	   typename _Traits>
    auto
    _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
	       _H1, _H2, _Hash, _RehashPolicy, _Traits>::
    erase(const_iterator __it)
    -> iterator
    {
      __node_type* __n = __it._M_cur;
      std::size_t __bkt = _M_bucket_index(__n);

      // Look for previous node to unlink it from the erased one, this
      // is why we need buckets to contain the before begin to make
      // this search fast.
      __node_base* __prev_n = _M_get_previous_node(__bkt, __n);
      return _M_erase(__bkt, __prev_n, __n);
    }

template<typename _Key, typename _Value,
	   typename _Alloc, typename _ExtractKey, typename _Equal,
	   typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
	   typename _Traits>
    auto
    _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
	       _H1, _H2, _Hash, _RehashPolicy, _Traits>::
    _M_erase(size_type __bkt, __node_base* __prev_n, __node_type* __n)
    -> iterator
    {
      if (__prev_n == _M_buckets[__bkt])
	_M_remove_bucket_begin(__bkt, __n->_M_next(),
	   __n->_M_nxt ? _M_bucket_index(__n->_M_next()) : 0);
      else if (__n->_M_nxt)
	{
	  size_type __next_bkt = _M_bucket_index(__n->_M_next());
	  if (__next_bkt != __bkt)
	    _M_buckets[__next_bkt] = __prev_n;
	}

      __prev_n->_M_nxt = __n->_M_nxt;
      iterator __result(__n->_M_next());
      this->_M_deallocate_node(__n);
      --_M_element_count;

      return __result;
    }

  實現上,根據迭代器找到指定的元素后,不僅將元素從容器中刪除,同時釋放了存儲該元素的動態內存。此時++iter引用了已被釋放的內存,引發heap-use-after-free錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM