STL容器介绍——vector的源码剖析和模拟实现

写在开头,此文章参考:侯捷——《STL源码剖析》

1.vector概述:

相较于我们熟悉的数组(array)来说,vector的操作方式和它几乎毫无差别。但回到了C++这门语言上,这门在内存空间上苛刻要求的情况下,vector则友好得多。
学过数据结构的同学们都知道,这其实就差不多是个顺序表———一个可自动配置新空间的array。好处就在于,它是一个动态空间,随着新的元素的加入,它内部可以自行扩张空间(capacity)以容纳下新的元素,我们也就不必害怕空间不足而开始就花费大量的内存去开辟一个很大的array了。

关于vector在空间上的配置问题:这是一个庞大的工程——“配置新空间—>数据移动—>释放旧空间”。
这边会引出一个待会会讨论到的问题——“迭代器失效”
因为我们并不是在原空间上继续对这个空间进行一系列操作,而是以原大小的两倍另外配置一块较大的空间,然后进行拷贝+释放。

2.关于vector的迭代器

由于vector本质就是一个连续线性空间的顺序表,可以说,用普通指针就可以作为vector的迭代器而满足所有必要的条件(list、map等就不行)
所以很多迭代器所操作的行为,普通指针天生就具备(如operator*,operator++,operator–…)
这里会模拟实现一下operator[ ]

先看看源码里面,开发者团队是怎么考虑和实现迭代器的吧

template <class T, class Alloc = alloc>
class vector {
public:
   typedef T value_type;
   typedef value_type* iterator;
   ...
   };

这里,iterator重载了 ,其迭代器指向vector内部的元素数据,可以看作是一个指针,但又不完全是( 因为指针和内存是挂钩的,并不会因为数据的移动和删除出现访问错误,但是迭代器会出现迭代器失效的问题)*

用《STL源码剖析》中的例子,我们可以来看一个声明:

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

总结一下迭代器的定义和声明,我们也写一个自己的迭代器
功能嘛,既然我们了解到vector的迭代器其实差不多就是一个普通指针的话,我们也不大需要用它来递增、递减、取值、成员存取等操作。
话不多说,动手!

定义:

template <class T>//这里没有用到空间配置器alloc
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		...
	};

实现接口

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

记住这四个接口哈,之后会反复用到。

3.vector的数据结构

看到刚刚的代码,可能之前没看过源码的同学会发出疑问:_start是啥,_finish是啥??
不要急,我这就解释。
(上面的代码毕竟是已经总结完写的)

template <class T,class Alloc = alloc>
class vector{
...
protected:
	iterator start;
	iterator finish;
	iterator end_of_storage;
...
}

这是源码中vector的定义,用C语言写过顺序表的同学第一眼见到这样的结构会被惊讶到,之前一般都是先定义一个指针,一个可用数据长度(size),一个空间大小(capacity)

我第一次见也很觉得厉害,作者为了让我们更好理解
STL容器介绍——vector的源码剖析和模拟实现_第1张图片
可以看到,利用好这三个迭代器,可以很好的得到顺序表的信息——目前使用空间的头,目前使用数据的长度(size),总空间大小(capacity)。

源码如下:

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()); }
  bool empty() const { return begin() == end(); }
  reference front() { return *begin(); }
  reference back() { return *(end() - 1); }
  reference operator[](size_type n) { return *(begin() + n); }
};

注:其中关于reference和size_type的声明和定义可以去源码中翻看解释

总结一下,自己也写个“阉割版”

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

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

		bool empty() const
		{
			return _start == _finish;
		}

		T& operator[](size_t i)
		{
			assert(i < size());

			return _start[i];
		}

		const T& operator[](size_t i) const
		{
			assert(i < size());

			return _start[i];
		}

4.vector的构造和内存管理

关于构造,源代码中给出了以下三种方法:
vector() : start(0), finish(0), end_of_storage(0) {}
vector(size_type n, const T& value) { fill_initialize(n, value); }
explicit vector(size_type n) { fill_initialize(n, T()); }

这其中的fill_initialize()是源码实现的一个函数,其功能是填充并予以初始化

void fill_initialize(size_type n, const T& value) {
    start = allocate_and_fill(n, value);
    finish = start + n;
    end_of_storage = finish;
    }
protected:
  iterator allocate_and_fill(size_type n, const T& x) {
      iterator result = data_allocator::allocate(n);
      uninitialized_fill_n(result, n, x);//STL提供的全局函数
      return result;
    }

其中uninitialized_fill_n()会根据第一参数的型别特性来决定使用算法fill_n或者反复调用construct()来完成任务。

分别对应:

vector<T> v1;
vector<T> v2(2,3);
vector<T> v3(2);
再来看看数据插入和删除(空间管理)

这个在结构上我们很好想:先看够不够空间,够的话直接拉高“水位”,不够就开空间,再拉高“水位”
就是这样一个思路,我们来看看源码大致是咋实现的:
这里直接用侯捷老师书中的解释最好了(其实是想偷懒=w=

STL容器介绍——vector的源码剖析和模拟实现_第2张图片
STL容器介绍——vector的源码剖析和模拟实现_第3张图片

这里代码太长了。
具体思路就差不多刚刚所讲:1.有备用空间的情况下,在备用空间的起始处增添一个新元素,finish迭代器++
2.没有备用空间的情况下,重新开一个空间大小为原大小两倍的空间,然后转移数据,再将原空间还给操作系统。

模拟实现

这里再自己模拟实现的时候,想到了stl其实还有两个接口
——resize()和reserve() 这两个函数作用都是开辟一个新的空间,区别就是resize()在开辟新空间后还会进行初始化

在不用到空间配置器的情况下,我们其实也可以手动实现这两个接口,同样可以达到同样开辟内存的效果

具体思路:空间是否够—>开辟新空间—>转移—>还内存

		void resize(size_t n, T val = T())//这里构造匿名对象T()
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}

				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					//memcpy(tmp, _start, sz*sizeof(T));
					//这里不要使用memcpy进行拷贝,因为memcpy实现的
					//是浅拷贝,拷贝到自定义类型会出现错误
					for (size_t i = 0; i < sz; ++i)
					{
						tmp[i] = _start[i];
					}

					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}

实现了这两个功能后,我们也可以实现一下push_back()这类的接口了

		void push_back(const T& x)
		{
			if (_finish == _endofstorage)//如果空间不够
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);//开辟新空间,转移数据,delete旧内存
			}*_finish = x;
			++_finish;
		}

5.vector的元素操作:pop_back,erase,clear,insert

源码和解释如下:

 void pop_back() {
    --finish;//很好想,拉水位嘛
    destroy(finish);//destroy是一个全局函数,就是一个析构,一种是接受一个指针,一个是接受迭代器
  }
  
	//清除一个pos位置的数据,结合algorithm中的find()使用
  iterator erase(iterator position) {
    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;
  }
  void clear() { erase(begin(), end()); }
  //复用上面的erase(first,last)
  
  

以下是insert实现的内容

**template <class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x) {
  if (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) {
        uninitialized_copy(finish - n, finish, finish);
        finish += n;
        copy_backward(position, old_finish - n, old_finish);
        fill(position, position + n, x_copy);
      }
      else {
        uninitialized_fill_n(finish, n - elems_after, x_copy);
        finish += n - elems_after;
        uninitialized_copy(position, old_finish, finish);
        finish += elems_after;
        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 {
        new_finish = uninitialized_copy(start, position, new_start);
        new_finish = uninitialized_fill_n(new_finish, n, x);
        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 */
      destroy(start, finish);
      deallocate();
      start = new_start;
      finish = new_finish;
      end_of_storage = new_start + len;
    }
  }
}**

这里稍微说一下实现思路:
这里要分情况来讨论:我们新增元素的个数和备用空间的关系

插入点之后的现有元素个数>新增元素个数
插入点之后的现有元素个数 ≤ 新增元素个数

思路很简单,要开新空间和不开新空间而已。和上述接口差不多,但是要注意一些细节
比如分开讨论 n是否为0的情况

好了,自己写一个看看呗:

void pop_back()
		{
			assert(!empty());
			--_finish;
		}

		void insert(iterator pos, const T& x)
		{
			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;

				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				// 更新pos,解决增容后pos失效的问题
				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end+1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
		}

		iterator erase(iterator pos)
		{
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it-1) = *it;
				++it;
			}

			--_finish;

			return pos;
		}
好了重头戏来了,回到开头说的一个问题——迭代器失效

啥是迭代器失效?顾名思义,就是达不到我们要的效果,甚至会发生错误
这里放在这里说,是因为我们讲到了erase和insert这两个接口

开头我们说过,所谓动态增加大小,并不是在原空间之后持续新空间(因为无法保证原空间之后上有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过去,然后才开始在原内容之后构造新元素,并释放原空间。因此,如果对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。

总结有以下两点:1.迭代器失效可能会导致结果的意义变了
2.迭代器失效可能会导致内存发生访问错误

这样理解:删除元素后由于被删除元素后面的数据都会发生移动,所以后面的迭代器都会失效。故上述代码在删除了某个迭代器后,后面的++it遍历已经失去意义,不会得到正确的结果

解决方法:由于我们知道,刚刚写的erase返回的是一个迭代器,指向的是pos(删除元素的下标)的下一个位置,那么这个就是更新之后的pos

假设有下面这个测试用例:

	void test_vector4()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);

		// 删除掉所有的偶数
		vector<int>::iterator it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			v.erase(it);
		}
}

这样的代码就会出现迭代器失效的问题
如何修改呢?按照上述的问题分析,我们可以很好想到

	vector<int>::iterator it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{	
				it = v.erase(it);//更新Pos
			}
			else
			{
				++it;
			}
		}

这样就可以搞定了~

以上,就是全部vectr源码的剖析内容了,感谢观看,如有错误,请及时在评论区纠正和批评哈www

你可能感兴趣的:(c++,算法,stl,数据结构)