C++ 之 vector 容器


特性

  1. vector是表示可變大小數組的序列容器。
  2. 就像數組一樣,vector也採用的連續存儲空間來存儲元素,說明可以採用下標對vector的元素進行訪問,和數組一樣高效。但是又不像數組,它的大小是可以動態改變的,而且它的大小會被容器自動處理。
  3. 本質上vector使用動態分配數組來存儲它的元素。當新元素插入時候,這個數組需要被重新分配大小爲了增加存儲空間。其做法是,分配一個新的數組,然後將全部元素移到這個數組。就時間而言,這是一個相對代價高的任務,因爲每當一個新的元素加入到容器的時候,vector並不會每次都重新分配大小。
  4. vector分配空間策略:vector會分配一些額外的空間以適應可能的增長,因爲存儲空間比實際需要的存儲空間更大。不同的庫採用不同的策略權衡空間的使用和重新分配。但是無論如何,重新分配都應該是對數增長的間隔大小,以至於在末尾插入一個元素的時候是在常數時間的複雜度完成的。
  5. 因此,vector佔用了更多的存儲空間,爲了獲得管理存儲空間的能力,並且以一種有效的方式動態增長。
  6. 與其它動態序列容器相比(deques, listforward_lists), vector訪問元素的時候更加高效,在末尾添加和刪除元素相對高效。對於其它不在末尾的刪除和插入操作,效率更低。比起listforward_list統一的迭代器和引用更好。

迭代器失效問題

vector容器的迭代器
在這裏插入圖片描述

  • 那麼什麼是vector容器的失效問題?
  • 是指對vector進行插入 / 刪除insert/erase)導致的迭代器指針非法現象。

代碼演示

int main(){
	int a[] = { 1, 2, 3, 4 };
	vector<int> v(a, a + sizeof(a) / sizeof(int));
	
	// 使用find查找3所在位置的iterator
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	
	// 刪除pos位置的數據,導致pos迭代器失效。
	v.erase(pos);	//現在的pos位置是一個失效位置
	cout << *pos << endl; // 此處會導致非法訪問
	
	// 在pos位置插入數據,導致pos迭代器失效。
	// insert會導致迭代器失效,是因爲insert可能會導致增容,增容後pos還指向原來的空間,而原來的空間已經釋放了。
	pos = find(v.begin(), v.end(), 3);
	v.insert(pos, 30);
	cout << *pos << endl; 	// 此處會導致非法訪問
	return 0;
}

常見的迭代器失效的場景(刪除vector中全部偶數):

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main(){
	int a[] = { 1, 2, 3, 4 };
	vector<int> v(a, a + sizeof(a) / sizeof(int));
	// 實現刪除v中的所有偶數
	// 下面的程序會崩潰掉,如果是偶數,erase導致it失效
	// 對失效的迭代器進行++it,會導致程序崩潰
	vector<int>::iterator it = v.begin();
	while (it != v.end()){
		if (*it % 2 == 0)
			v.erase(it);
		++it;
	}
	
	// 以上程序要改成下面這樣,erase會返回刪除位置的下一個位置
	vector<int>::iterator it = v.begin();
	while (it != v.end()){
		if (*it % 2 == 0)
			it = v.erase(it);		//使用迭代器 重新接收~
		else
			++it;
	}
	return 0;
}

源碼剖析

其實vector底層維護了三個成員參數,類型都是迭代器(原生指針):

  1. start:開始位置
  2. finish:實際結束位置
  3. end_of_storage:容量結束位置

end_of_storagefinish的差值就是備用位置

差值如果爲0了,說明二者相等,就要進行擴容操作了,這會涉及重新配置空間、數據拷貝、釋放舊空間等一系列動作,將會付出額外代價進行數據搬運。


模擬實現

實現一個Vector類,模擬vector容器的功能與特性。

#include <assert.h>

template<class T>
class Vector{
public:
	typedef T* iterator;
	typedef const T* const_iterator;

	Vector()
		:_start(nullptr)
		, _finish(nullptr)
		, _endofstorage(nullptr)
	{}

	//拷貝構造
	
	//	老式寫法:
	//Vector(const Vector<T>& v)
	//{
	//	_start = new T[v.Capacity()];
	//	memcpy(_start, v._start, v.Size()*sizeof(T));
	//	_finish = _start + v.Size();
	//	_endofstorage = _start + v.Capacity();
	//}

	//	新式寫法:
	Vector(const Vector<T>& v)
		:_start(nullptr)
		, _finish(nullptr)
		, _endofstorage(nullptr)
	{
		Reserve(v.Size());
		for (size_t i = 0; i < v.Size(); ++i){
			this->PushBack(v[i]);		//逐個丟進去~
		}
	}

	// v1 = v2
	Vector<T>& operator=(Vector<T> v){
		this->Swap(v);
		return *this;
	}

	void Swap(Vector<T>& v){	//只交換三個成員變量,代價遠低於使用算法交換對象
		std::swap(_start, v._start);
		std::swap(_finish, v._finish);
		std::swap(_endofstorage, v._endofstorage);
	}

	~Vector(){
		if (_start){
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}
	}

	iterator begin(){
		return _start;
	}

	iterator end(){
		return _finish;
	}

	const_iterator begin() const{
		return _start;
	}

	const_iterator end() const{
		return _finish;
	}

	void Reserve(size_t n){
		if (n > Capacity()){
			size_t size = Size();
			// 開新空間
			T* newarray = new T[n];
			if (_start)
				memcpy(newarray, _start, sizeof(T)*Size());

			// T的 operator=
			/*	for (size_t i = 0; i < size; ++i){
				newarray[i] = _start[i];
			}*/

			// 釋放舊空間
			delete[] _start;

			// 賦值
			_start = newarray;
			_finish = _start + size;
			_endofstorage = _start + n;
		}
	}

	void Resize(size_t n, const T& val = T()){		//缺省參數,匿名對象
		if (n <= Size()){
			_finish = _start + n;
		}
		else{
			Reserve(n);
			while (_finish != _start + n){
				*_finish = val;
				++_finish;
			}
		}
	}

	void PushBack(const T& x){
		if (_finish == _endofstorage){
				//我們選擇2倍增長,其實不是硬性規定
				//VS下是1.5倍增長,CentOS下是2倍增長
			size_t newcapacity = Capacity() == 0 ? 4 : Capacity() * 2;
			Reserve(newcapacity);
		}
		*_finish = x;
		++_finish;
	}

	void PopBack(){
		assert(_finish > _start);
		--_finish;		//標記爲無效數據即可~
	}

	void Insert(iterator pos, const T& x){
		assert(pos < _finish);

		if (_finish == _endofstorage){
			size_t n = pos - _start;
			size_t newcapacity = Capacity() == 0 ? 4 : Capacity() * 2;
			Reserve(newcapacity);
			pos = _start + n;
		}

		iterator end = _finish-1;
		while (end >= pos){
			*(end + 1) = *end;		//數據搬運,從後往前
			--end;
		}
		*pos = x;
		++_finish;
	}

	iterator Erase(iterator pos){
		assert(pos < _finish);

		iterator it = pos;
		while (it < _finish - 1){
			*it = *(it + 1);		//數據搬運:覆蓋
			++it;
		}
		--_finish;
		return pos;
	}

	size_t Size() const{
		return _finish - _start;
	}

	size_t Capacity() const{
		return _endofstorage - _start;
	}

	T& operator[](size_t pos){
		assert(pos < Size());
		return _start[pos];
	}

	const T& operator[](size_t pos) const{
		assert(pos < Size());
		return _start[pos];
	}

private:
	iterator _start;
	iterator _finish;
	iterator _endofstorage;
};

vector與list的對比

vector list
底層結構 動態順序表,一段連續空間 頭結點的雙向循環鏈表
隨機訪問 支持隨機訪問,訪問某個元素效率O(1) 不支持隨機訪問,訪問某個元素效率O(N)
插入和刪除 任意位置插入和刪除效率低,需要搬移元素,時間複雜度爲O(N),插入時有可能需要增容,(增容:開闢新空間,拷貝元素,釋放舊空間,導致效率更低) 任意位置插入和刪除效率高,不需要搬移元素,時間複雜度爲O(1)
空間利用率 底層爲連續空間,不容易造成內存碎片,空間利用率高,緩存利用率高 底層節點動態開闢,小節點容易造成內存碎片,空間利用率低,緩存利用率低
迭代器 原生態指針 對原生態指針(節點指針)進行封裝
迭代器失效 在插入元素時,要給所有的迭代器重新賦值,因爲插入元素有可能會導致重新擴容,致使原來迭代器失效,刪除時,當前迭代器需要重新賦值否則會失效 插入元素不會導致迭代器失效,刪除元素時,只會導致當前迭代器失效,其他迭代器不受影響
使用場景 需要高效存儲,支持隨機訪問,不關心插入刪除效率 大量插入和刪除操作,不關心隨機訪問

在這裏給出list篇的博客鏈接,感興趣的看官請移步至:【https://blog.csdn.net/qq_42351880/article/details/100111223

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