【STL】vector(五)

一、概述

vector的數據安排及操作方式與array很相似,二者唯一差別在於空間運用的靈活性。

  1. array是靜態空間,一旦配置了大小,就不能改變,要換個大或者小點的空間,得有客戶端自己來操作。
  2. vector是動態空間,隨着元素的加入,它的內部機制會自行擴充空間以容納新元素。

vector相對於array來說就是可以自己內部進行內存的動態縮減,vector這種操作有有點也有缺點。
【優點】
vector的運用對於內存的合理利用與運用的靈活性有很大的幫助,不必因空間不足而一開始就申請一大塊內存。
【缺點】
vector內部空間的擴充主要經歷三步:
1)配置新空間。
2)數據移動。
3)釋放舊空間。
這種行爲的時間成本很高。針對這個問題vector內部本身有一定的策略進行處理,同時使用者在使用的過程中也應該要有考慮到此問題並要有一定的處理策略。

二、迭代器

  1. vector維護的是一個連續性的空間。
  2. vector迭代器所需要的操作行爲,普通指針天生就具備,例如:
    operator*,operator->,operator++,operator–,operator+,operator-,operator+=,operator-=。
  3. vector支持隨機存儲,而普通指針也可以。

所以vector迭代器種類是Random Access Iterators。

template <class T, class Alloc = alloc>
class vector {
public:
  typedef T value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type* iterator; //Vector 的迭代器就是普通指針。
  ...
  }

如果客戶端定義如下代碼:

vector<int>::iterator ivite;
vector<Shape>::iterator svite;

ivite的型別就是int*,svite的型別就是Shape*。

三、數據結構

vector採用的數據結構非常簡單:線性連續空間。以兩個迭代器startfinish分別指向配置得來的連續空間中已被使用的範圍,並以迭代器end_of_storge指向整塊連續空間(含備用空間)的尾端。

template<class T, class Alloc = alloc>
class vector
{
...
protected:
	iterator start;  		//表示目前使用空間的頭
	iterator finish; 		 //表示目前使用空間的尾
	iterator end_of_storage; //表示目前可用空間的尾
...
}

爲降低空間配置時的速度成本,vector實際配置的大小可能比客戶端需求量更大一些,以備將來可能的擴充,這便是容量(capacity)的觀念,換句話說,一個vector的容量永遠大於或等於其大小。一旦容量等於大小,便是滿載,下次再有新增元素,整個vector就要開始進行新空間申請、元素的移動、舊空間的釋放等操作。
以start,finish,end_of_storage三個迭代器元素操作及容量計算:

template<class T, class Alloc = alloc>
class vector
{
...
public:
	iterator begin() { return start; }
    iterator end() { return finish; }
    size_type size() const { return size_type(end() - begin()); }
    size_type capacity() const { return size_type(end_of_storage - begin()); }
    bool empty() const { return begin() == end(); }
  	reference operator[](size_type n) { return *(begin() + n); }
  	reference front() { return *begin(); }
  	reference back() { return *(end() - 1); }
  	...
}

vector操作之後示意圖如圖2:
在這裏插入圖片描述

圖2

四、構造與內存管理

vector缺省使用alloc作爲空間配置器,爲方便以元素大小爲配置單位,定義了一個data_allocator。

template< class T, class Alloc = alloc>
class vector
{
	typedef simple_alloc<value_type, Alloc> data_allocator;
...	
}

於是data_allocator::allocate(n)表示配置n個元素空間。

【構造】
vector提供許多constructors,其中一個允許我們指定空間大小及初值:

//構造函數,允許指定vector大小n和初值value
vector(size_type n, const T& value){fill_initialize(n, value)}

//填充並予以初始化
void fill_initialize(size_type n, const T& value)
{
	start = allocate_and_fill(n, value);
	finish = start + n;
	end_of_storage = finish;
}

//配置後填充
iterator allocate_and_fill(size_type n, const T& x)
{
	iterator result = data_allocator::allocate(n); //配置n個元素空間
	uninitialized_fill_n(result, n, x); //全局函數
	return result;
}

處於性能考慮,uninitialized_fill_n會根據第一個參數的型別特性(用到參數類型萃取,可參考Traits編程技法),決定使用算法fill_n( ),或反覆調用construct( )來完成任務。

【內存管理】
當我們以push_back()將新元素插入vector尾端時,會做以下處理步驟:

  1. 首先檢查是否還有備用空間,如果有就直接在備用空間上構造元素,並調整迭代器finish,使vector變大。
  2. 如果沒有備用空間,就擴充空間(重新配置、移動數據、釋放原空間)。
void push_back(const T& x) 
{
    if (finish != end_of_storage) 
    {
      //還有備用空間
      construct(finish, x);
      //調整迭代器
      ++finish;
    }
    else
      insert_aux(end(), x);//已無備用空間。
}

template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x) 
{
	if (finish != end_of_storage) 
	 {
	   //還有備用空間
	   //在備用空間起始處構造一個元素,並以vector最後一個元素爲其初值
	   construct(finish, *(finish - 1));
	   //調整水位
	   ++finish;
	   T x_copy = x;
	   //拷貝移動數據
	   copy_backward(position, finish - 2, finish - 1);
	   *position = x_copy;
	 }
  	else 
	{
	    //已無備用空間
		const size_type old_size = size();
		const size_type len = old_size != 0 ? 2 * old_size : 1;
		//以上配置原則:如果原大小爲0,則配置1(個元素大小)
		//如果原大小不爲0,則配置原大小的兩倍。
		//前半段用來放置原數據,後半段用來放置新數據。
		iterator new_start = data_allocator::allocate(len);
		iterator new_finish = new_start;
		__STL_TRY 
		{
		    //將原vector的內容拷貝到新vector
			new_finish = uninitialized_copy(start, position, new_start);
			//爲新元素設置初值
			construct(new_finish, x);
			//調整水位
			++new_finish;
			//將安插點的原內容也拷貝過來
			new_finish = uninitialized_copy(position, finish, new_finish);
		}
	
		#ifdef  __STL_USE_EXCEPTIONS 
		catch(...) 
		{
			destroy(new_start, new_finish); 
			data_allocator::deallocate(new_start, len);
			throw;
		}
		#endif /* __STL_USE_EXCEPTIONS */
		//析構並釋放原vector
		destroy(begin(), end());
		deallocate();
		
		//調整迭代器,指向新vector
		start = new_start;
		finish = new_finish;
		end_of_storage = new_start + len;
	}
}

上述push_back()進行了備用空間大小檢查,insert_aux()也進行了備用空間檢查,那是因爲insert_aux()不僅僅給push_back()使用,還被其它調用這使用。

【1】插入單個元素時如果空間不足,申請新空間規則如下:

insert_aux(iterator position, const T&)
{
...
	const size_type old_size = size();
	const size_type len = old_size != 0 ? 2 * old_size : 1;
...
}

【2】插入批量數據時如果空間不足,申請新空間規則如下:

insert(iterator position, size_type n, const T& x)
{
...
	 const size_type old_size = size();        
     const size_type len = old_size + max(old_size, n);
...
}

所謂動態增加大小,並不是在原空間之後接續新空間(因爲無法保證原空間之後尚有可供配置空間),而是心申請特定大小的空間,然後將原內容拷貝過來,並釋放原空間。因此對vector的任何操作,一旦引起空間重新配置,指向原vector的所有迭代器就都失效了,這個要多注意。

五、元素操作

vector提供的元素操作很多,爲搭配上文對內存管理的操作討論,這裏只講解下erase和insert。

5.1 erase()

【清除某個位置上的元素】

 //清除某個位置上的元素
 iterator erase(iterator position)
 {
    //1、如果清除的不是最後一個元素,就將要清除位置之後的元素向前移動,然後刪除掉最後一個元素。
    //2、如果清除的是最後一個元素,直接調整finish位置,然後清除最後一個元素即可。
    if (position + 1 != end())
      copy(position + 1, finish, position);
    --finish;
    destroy(finish);
    return position;
  }

【清除某個區間中的所有元素】

//清除[first, last)中的所有元素,前閉後開區間
 iterator erase(iterator first, iterator last) 
 {
    iterator i = copy(last, finish, first);
    destroy(i, finish);
    finish = finish - (last - first);
    return first;
  }

圖5-1展示了erase(first, last)的操作。
在這裏插入圖片描述

圖5-1
5.2 insert()
5.2.1 源碼

從position開始,插入n個元素,元素初值爲x。

template <class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x) 
{
  if (n != 0) 
  {
    //n非0,纔會進行如下操作
    if (size_type(end_of_storage - finish) >= n) 
    {
        //備用空間大於等於“新增元素個數”
		T x_copy = x;
		//計算插入點之後的現有元素個數
		const size_type elems_after = finish - position;
		iterator old_finish = finish;
		if (elems_after > n) 
		{
		    //“插入點之後的現有元素” 大於 “新增元素個數”
		    //1、將finish前面的n個元素拷貝到finish後面(內部會考慮是一個一個拷貝構造還是
		    //直接移動內存內容來實現複製行爲)。
			uninitialized_copy(finish - n, finish, finish);
			//2、調整水位線。
			finish += n;
			//3、直接[position, old_finish-n]以後拷貝方式拷貝到之前的finish位置。
			copy_backward(position, old_finish - n, old_finish);
			//4、開始填充新元素。
			fill(position, position + n, x_copy);
			
			<!注意:這裏爲什麼不直接將position之後的元素移動拷貝到相應位置之後呢?
			<!只所以不這樣操作是處於內存效率考慮,因爲針對未初始化的區域涉及到內存的配置和元素的構造,
			<!如果是已知區域,可直接調用copy_backward(),省略內存的配置這一步操作。
		}
		else 
		{
		    //“插入點之後的現有元素個數” 小於等於 “新增元素個數”
		    //1、先在finish位置之後,填充n-elems_after個元素
			uninitialized_fill_n(finish, n - elems_after, x_copy);
			//2、調整finish位置
			finish += n - elems_after;
			//3、將[position, old_finish)範圍內的元素複製到新finish之後
			uninitialized_copy(position, old_finish, finish);
			//4、再次調整finish位置
			finish += elems_after;
			//5、插入剩餘的x_copy元素
			fill(position, old_finish, x_copy);
		}
    }
    else 
    {
        //備用空間小於“新增元素個數”(那就必須配置額外的內存)
        //首先決定新長度:舊長度的兩倍,或舊長度+新增元素個數
		const size_type old_size = size();        
		const size_type len = old_size + max(old_size, n);
		iterator new_start = data_allocator::allocate(len);
		iterator new_finish = new_start;
		__STL_TRY 
		{
		    //1、將[start, position)元素拷貝到新內存區域
			new_finish = uninitialized_copy(start, position, new_start);
			//2、從new_finish開始填充n個元素個數
			new_finish = uninitialized_fill_n(new_finish, n, x);
			//3、將[position,finish)區間元素拷貝到新內存區域
			new_finish = uninitialized_copy(position, finish, new_finish);
		}
  #ifdef  __STL_USE_EXCEPTIONS 
      	catch(...) 
		{
		    //如果有異常發生,實現“commit or rollback” semantics
			destroy(new_start, new_finish);
			data_allocator::deallocate(new_start, len);
			throw;
		}
  #endif /* __STL_USE_EXCEPTIONS */
      //釋放舊內存區域元素
      destroy(start, finish);
      //釋放舊內存
      deallocate();
      //調整新的水位線
      start = new_start;
      finish = new_finish;
      end_of_storage = new_start + len;
    }
  }
}
5.2.2 操作過程

【備用空間 >= 新增元素個數】
備用空間 2,新增元素個數 2.

【1】插入點之後的現有元素個數3 > 新增元素個數,操作如圖5-2-1。
在這裏插入圖片描述

圖5-2-1

【2】插入點之後的現有元素個數 2 <= 新增元素個數3,操作如圖5-2-2。
在這裏插入圖片描述

圖5-2-2

【備用空間 < 新增元素個數】
新插入元素個數n = 3,此時要重新申請一塊內存區域。操作如圖5-2-3
在這裏插入圖片描述

圖5-2-3
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章