1. 程式人生 > >C++ vector中的迭代器失效問題

C++ vector中的迭代器失效問題

vector中的迭代器失效問題
在使用vector的成員函式時,有兩個成員函式內部會出!](https://img-blog.csdnimg.cn/20181124093029161.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01famlhbmppYW5qaWFv,size_16,color_FFFFFF,t_70)現迭代器失效的問題。分別是insert和erase而這兩個成員函式迭代器失效的原因並不相同。
insert

在我們使用insert的介面時,不會遇到迭代器的失效問題的影響,因為在insert中已經處理過l。而此處所說的就是在insert實現過程中出現的迭代器失效問題。

insert(iterator pos,value_type value);
vector類中的insert函式,在所傳迭代器位置的數前面插入一個數,返回值是所插入位置的迭代器。

現在我們對於該函式進行模擬實現,插入時有兩種情況

  • 插入後長度不會超出當前容量
  • 插入後超出當前容量

對於第一種,在容量足夠的情況下插入,進行正常的元素挪動和插入就行了。

對於第二種,當插入後的長度超出了容量的範圍。此時我們就需要先進行擴容。

如何擴容?

  擴容時因為此時容量已不夠用,我們需要重新開闢一塊大於當前容量的空間。至於開闢多大的容量,這個我們可以自定義。只要能容納所插入的值。在vs下每次擴容的大小是之前容量的1.5倍,而在linux g++下每次開闢的容量是之前容量的2倍。開闢新空間後,將原空間的內容拷貝至新空間即可,再將物件中的指標指向新空間,再釋放原空間即可即可。此時應該注意,需要進行深拷貝,如果當vector中的元素為string vector時,如果進行是淺拷貝,在析構的時候會發生錯誤。

完成擴容後,此時卻出現了一個問題,我們所傳的迭代器位置是指向原空間的,而原空間的位置在我們進行擴容時已經釋放了,那意味著原來的迭代器位置已經是一個野指標,此時原先的迭代器位置已將失效。

如何解決?

我們只需在擴容時,將原迭代器位置的偏移量記錄下來,在新開闢的空間中找到原有的位置即可。

insert 的模擬實現

Iterator Insert(Iterator pos,T value)
    {
      assert(pos<=_finish);
      
      size_t offset=pos-_start;//增容更換空間後pos處的迭代器會失效,記錄其偏移量

      if(_finish == _endofstorage)
      {
        size_t newCapacity = Capacity()==0?2:2*Capacity(); //防止第一次為空
        Reserve(newCapacity);
      }

      pos = begin() + offset;
      Iterator end =this->end();
      while(end != pos)
      {
        *end = *(end-1);
         end--;
      }

      *pos = value;
      _finish++;
      return pos;

    }

erase

在vector中erase 也會導致迭代器失效,不同於insert是在內部實現時會遇到的空間的更換造成迭代器失效,不會對於我們使用時造成影響,但erase會在使用時對我們造成一些影響。

iterator erase(iterator pos);
返回所刪除的數的下一個數的迭代器
int arr[] = { 1, 2, 3, 4, 5, 6 };
	vector<int> nums(arr, arr + sizeof(arr) / sizeof(int));


	vector<int>::iterator pos;
	
	 pos = nums.insert(nums.begin() + 1 , 9);

	 nums.erase(pos);
	 
	 *pos = 10;  //此處報錯

當我們刪除一個數後,再通過迭代器位置去訪問時,就會報錯
在這裡插入圖片描述

為甚麼會報錯
當我們刪除後將一個數刪除後者個位置的迭代器也就被刪除了,而我們如果再對刪除的迭代器位置進行修該訪問就會報錯

	 pos=nums.erase(pos);
	//只要我們重新接收erase返回的迭代其位置,就可以了
	 *pos = 10;
	 
	 //此時不會報錯

通過除錯,我們會發現,pos的傳入地址和傳出的地址完全一樣,並沒有發生變化
在這裡插入圖片描述
在這裡插入圖片描述

為甚麼迭代器會失效
通過vs底層實現的,我們可以看到他返回的迭代器不僅僅是傳入的位置,還進行了處理,可見vs在對迭代器進行了處理,以便於檢查,只要迭代器被刪除過後,就不能再使用,只能接受它返回的正確的迭代器。在這裡插入圖片描述

從以下的底層程式碼可以看到vs對迭代器進行的檢查,在每次使用迭代器之前都對迭代器進行檢查。
在這裡插入圖片描述
將同樣的程式碼放在Linux下進行執行

int main()                          
  7 {                                   
  8   int arr[] = { 1, 2, 3, 4, 5, 6  };
  9     vector<int> nums(arr, arr + size
    of(arr) / sizeof(int));             
 10                                     
 11                                     
 12       vector<int>::iterator pos;    
 13                                     
 14          pos = nums.insert(nums.begi
    n() + 1 , 9);                       
 15                                     
 16            nums.erase(pos);         
 17                                     
 18              *pos = 10;             
 19                                     
 20              for(auto& e: nums)     
 21                cout<<e<<" ";        
 22              cout<<endl;            
 23                                     
 24                                     
 25   return 0;
[[email protected] test2]$ ./test 
1 10 3 4 5 6 

在linux 下,程式碼正常執行並不會報錯,結果正確。這是因為在linux下對於迭代器的檢查並不嚴格,所以只要不超過迭代器的訪問區間,Linux下一般不會報錯。但是有時候我們必須要注意所得結果的正確性。

linux下的erase 的底層原始碼

template<typename _Tp, typename _Alloc>
133	    typename vector<_Tp, _Alloc>::iterator
134	    vector<_Tp, _Alloc>::
135	    erase(iterator __position)
136	    {
137	      if (__position + 1 != end())
138		_GLIBCXX_MOVE3(__position + 1, end(), __position);
139	      --this->_M_impl._M_finish;
140	      _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
141	      return __position;
(gdb) 
142	    }
//我們可以看到傳入和傳出的是一個position,而且也沒有太多的檢查

而在Linux的底層中在返回時並沒有對迭代器進行處理,在進入時也並沒有對於原迭代器進行檢查,只是對於迭代器的範圍進行了檢查。所以在不同的平臺下,檢查的方式不一樣,嚴格程度不一樣所以可能得出的結果也不同。

我們再來看這樣一份程式碼
int main()
{


	int arr[] = { 1, 2, 3, 4, 5, 6 };
	vector<int> nums(arr, arr + sizeof(arr) / sizeof(int));

	vector<int>::iterator it = nums.begin();
	while (it != nums.end())
	{
		if (*it % 2 == 0)
			nums.erase(it);
		++it;
	}

	for (int i = 0; i<nums.size(); i++)
		cout << nums[i] << " ";
	return 0;
}

毫無疑問在vs地下會報錯,在刪完2後,對it進行++時就會報錯。

但在linux下則會出現多種情況:

  • 當我們所給刪除序列為 1 2 3 4 5 6

程式會報出段錯誤

[[email protected] test_erase]$ ./test 
Segmentation fault (core dumped)

通過gdb除錯我們可以看到
在我們刪除2,4,之後,在進行6的刪除的時候因為,超出了迭代器的訪問位置而導致的段錯誤。

  • 當我們所給刪除序列為 1 2 3 4 5
[[email protected] test_erase]$ ./test
1 3 5 [[email protected] test_erase]$

程式執行正常,且結果正確。

  • 當我們所給刪除序列為 1 2 8 4 6 5
[[email protected] test_erase]$ ./test
1 8 6 5 [[email protected] test_erase]$ 

可以看到,在刪除的時候,並沒有將偶數項全部刪除完,是是因為在刪除過程中,會將後面的向前覆蓋,而返會的位置是當前的位置,下一次++時會將該位置跳過去,所以會出現這種錯誤。

在給出的三種錯誤種,我們可以看到有時侯結果執行正確,有時候錯誤,有時候程式掛掉。所以我們不能在有些時候因為結構正確就認為程式也是正確的。

所以erase 在配合迴圈使用時要注意
下面為正確的些法

int main()
{


	int arr[] = { 1, 2, 3, 4, 5, 6 };
	vector<int> nums(arr, arr + sizeof(arr) / sizeof(int));

	vector<int>::iterator it = nums.begin();
	while (it != nums.end())
	{
		if (*it % 2 == 0)
		    it=nums.erase(it);  //使用一個迭代器來對返回的迭代器進行接受 
		else 
		    ++it; //因為返回的還是原位置,所以進行判斷的時候防止跳過
	}

	for (int i = 0; i<nums.size(); i++)
		cout << nums[i] << " ";
	return 0;
}
總結:以上所說即為vector中的迭代器失效問題,一種是因為空間更換後造成的迭代器失效,一種是因為刪除後造成的迭代器失效。而在vs和linux下,因為對於底層的程式碼和檢查機制與程度不同,所以執行得出的結果也不一樣。所以,在對於erase進行使用時,要注意迭代器的位置方面的問題,防止因此引發的一些問題。