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

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