STL源码刨析_vector

目录

一. vector 介绍

二. vector 模拟实现

1. vector的参数

2. 默认构造函数

3. size

4. capacity

5. reserve

6. resize

8. 构造函数

9. iterator

10.  push_back

11. insert

12. erase

13. operator[]


一. vector 介绍

STL(Standard Template Library)中的vector是一种常用的动态数组容器。它在内存中按顺序存储元素,并可根据需要自动调整容量。以下是一些关于vector容器的介绍:

  • 动态数组:vector容器可以根据需要动态增长或减少其大小。它能够在运行时自动调整内部数组的大小,使其能够容纳任意数量的元素。
  •  随机访问:通过索引可以快速随机访问vector中的元素。vector以连续的内存块存储元素,因此可以使用索引来访问、修改或删除元素,时间复杂度为O(1)。
  •  自动内存管理:vector会自动管理其内部的内存空间,无需手动分配或释放内存。当添加元素数量超出当前容量时,vector会自动重新分配更大的内存块,并将原有元素复制到新的内存块中。
  •  元素的插入和删除:插入和删除元素的操作比较高效,通过在指定位置插入或删除元素,其他元素会自动移动以保持连续存储。
  •  可以存储任意类型的元素:vector可以存储任意类型的元素,包括内置类型(如整数、浮点数)和自定义类型。要存储自定义类型的元素,需要提供适当的构造函数和析构函数,以及重载运算符等必要的函数。
  •  提供丰富的成员函数和算法:vector提供了许多成员函数,可以方便地对元素进行操作,如插入、删除、访问等。它还与STL的其他组件(如算法)配合使用,可以进行排序、搜索、遍历等更复杂的操作。

总体上,vector是一个灵活、高效、易于使用的容器,用于存储和操作动态大小的元素序列。它是C++标准库(STL)中最常用的容器之一。

二. vector 模拟实现

这里说明一下,我们模拟实现的 vector 不会实现库里面的函数,只是实现一些常用的函数,帮助我们理解 vector 的底层

1. vector的参数

 

vector 是一个模板,所以我们的里面存储的内热当然也是需要变动的,我们需要用到我们的模板来进行声明变量,而我们的vector 里面也是有三个参数

  1. 指向数据的指针
  2. size
  3. capacity

但是我们库里面的 vector 的三个参数都是用指针来表示的

template
	class vector
	{
	public:

	private:
		iterator _start; // 起始位置
		iterator _finish; // size
		iterator _endofstorage; // capacity
	};
}

这里说明一下为什么可以用指针代表上面的size 和 capacity

首先,_start 是不用说明的,他就是指向存储数据的一个指针,而_finish 表示存的数据的最后一个位置的后一个位置,_endofstorage表示最大存储容量的后一个位置,而我们知道指针-指针就是代表中间的数据个数,所以我们的 _finish - _start 就表示 size 而我们的 _endofstorage- _start 就表示 capacity

2. 默认构造函数

实现思路:默认的构造函数就是一个无参的构造函数,但是库里面的容器都有一个缺省的空间适配器,我们这里没有,所以我们这里的无参构造就是一个无参的函数,而我们的默认构造函数里面我们也不需要干什么,我们也不需要开空间,我们只需要把里面的参数初始化就好了,我们的成员变量是三个指针,所以我们可以初始化为 nullptr

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

3. size

size 该函数就是返回我们的数据个数,我们前面说了,我们只需要返回 _finish - _start 就可以了,因为我们的指针 - 指针就是中间的数据个数

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

4. capacity

capacity 返回当前最大的存储容量,我们还是可以使用 _endofstorage - _start 来计算出最大存储容量

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

5. reserve

reserve 就是我们要开空间,那么我们怎么开空间呢? new 我们可以 new T (模板)类型的对象,然后我们将原来的值拷贝进去,如果原来没有值当然不需要拷贝,然后设置它的 _finish 和 _endofstorage。

但是着还并没有完,我们还是有很多细节的,我们在扩容的时候,如果我们(_start)是一个空呢? 我们如果直接扩容,然后修改 _start 的指向,那么我们的_finish 和 _endofstorage 呢? 所以我们是需要在扩容后修改 _finish 和 _endofstorag,但是光这样还是不够的,我们假设我们现在又数据,然后我们扩容后修改_finish 那么我们要怎么修改?我们可不可以调用 size 来确认需要将 _finish 修改到那里? 这里是不可以的,因为如果我们先修改 _start 的话,我们的 size 调用是调用 _finish - _start 来计算的,如果我们先修改了 _start 在修改 _finish 的话我们就会出错,所以我们这里又两种解决方法

  1. 我们先修改 _finish 在修改 _start 
  2. 我们先计算出 old_size 然后在修改 _start 和 _finish 的值

上面为什么没有说 _endofstorage 呢? 因为我们的 _endofstorage 不会被影响

还没有结束,这里还有一条,既然我们的 vectoe 里面可以存储任何类型,那么我们当然是不可能只存储 内置类型,我们还会存储一些自定义类型,所以这时候问题就出来了,如果我们的自定义类型是需要深拷贝的呢?

如果我们的自定义类型需要深拷贝,但是我们又只是对 vector 进行了开空间,然后我们对 vector 里面的数据进行了浅拷贝,那么我们的 vector 里面的对象就出问题了,在虚构的时候我们会析构两次,然后出错,所以我们不能对 vector 的拷贝构造函数只进行浅拷贝,我们需要调用自定义类型对象的赋值重载。

下面详细看代码

		void reserve(size_t n)
		{
			// 检查是否需要扩容
			if (n > capacity())
			{
				// record old_size
				size_t old_size = _finish - _start;
				iterator tmp = new T[n];
				// 拷贝数据
				//memcpy(tmp, _start, sizeof(T) * size());
				for (int i = 0; i < old_size; ++i)
				{
					tmp[i] = _start[i]; // 调用内置/自定义类型对象的赋值重载
				}
				_start = tmp;
				// update _finish and _endofstorage
				_finish = _start + old_size;
				_endofstorage = _start + n;
			}
		}

6. resize

我们先可以看一下 resize 的参数

		void resize(size_t n, const T& val = T());

我们的 resize 又两个参数其中一个是 想要设置的大小,另外一个是设置的值,如果原本有值,那么不会修改原本的值,如果原本没有值,会设置为传入的值,如果 n 小于 size 那么就会缩小 size 为 n,如果 n 大于capacity 那么也会起到扩容的作用,然后还回填值,所以扩容的话我们可以直接复用 reserve 函数,然后扩容后在填值

		void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
					reserve(n);

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

7. 拷贝构造

我们先看一下参数

		vector(const vector& v);

我们的拷贝构造就是拷贝一个和参数一样的值,那么我们的拷贝构造如何实现?

我们的拷贝构造我们需要开一块和 v 一样的值,所以但是这里和我们的 reserve 是一样的,如果我们仅仅只是对于 v 里面对象进行浅拷贝,那么我们肯定是行不通的,所以我们还是需要进行深拷贝,也就是调用v中对象的赋值重载函数

		vector(const vector& v)
		{
			_start = new T[v.capacity()];
			for (int i = 0; i < v.capacity(); ++i)
			{
				_start[i] = v._start[i];
			}
			_finish = _start + v.size();
			_endofstorage = _start + v.capacity();
		}

8. 构造函数

这里我们要说两个构造函数,还是先看一下这两个函数

		vector(size_t n, const T& val = T());
		vector(int n, const T& val = T());

首先是这个构造函数,我们创建了两个 一个是 size_t 的一个是 int 的,等我们讲完另一个构造函数后说这个问题,但是这两个函数的内部是没有一点差别的,只是参数有一点差别

我们的这两个函数的参数的意思就是定义的时候开多少个空间,用什么值来初始化,如果不传值的话,那么就是使用 T 类型的默认构造了(这里说明一下,在C++里面加入了模板之后 C++就把内置类型升级了,让内置类型也有构造函数)

		vector(size_t n, const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			_start = new T[n];
			for (int i = 0; i < n; ++i)
			{
				_start[i] = val;
			}
			_finish = _start + n;
			_endofstorage = _start + n;
		}

		vector(int n, const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			_start = new T[n];
			for (int i = 0; i < n; ++i)
			{
				_start[i] = val;
			}
			_finish = _start + n;
			_endofstorage = _start + n;
		}

下面在说一下另外一个构造函数

		template
		vector(InputIterator first, InputIterator last);

我们的这个构造函数时可以通过迭代器来初始化该对象的,但是如果我们只是用 iterator (也就是vector 里面 typedef 的 iterator)那么我们就只能用 vector 的迭代器来初始化 vector 对象,所以我们不能只式使用一种类型的迭代器,所以我们可以在该函数前面加一个模板,让我们传参去自动推导,剩下的就是不是什么问题了

		template
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

好了,这里我们说一下为什么需要了两个前面的构造函数,我们先看一下下面他们两个的的参数

		vector(size_t n, const T& val = T());
		template
		vector(InputIterator first, InputIterator last)

我们的这两个函数的参数是这样的,那么我们看起来是并没有冲突的,但是我们可以想一下,如果我们现在传入两个值(10,1),那么这个传值会调用哪个函数?

这里我直接说了,他会调用下面的那个,但是为什么呢?我们知道在C++中的我们函数传值的话会自动挑选最匹配的那个,我们如果传值是(10,1),那么我们的模板会推导为int类型,但是我们上面的那个函数一个是 size_t 一个是 模板,所以我们的 编译器会认为下面的函数会更匹配,所以就会调用下面的函数,所以我们的解决方法就是我自己手动写一个更匹配的,所以就有了下面的 int 的函数

9. iterator

我这里就把 iterator 的函数一起说了,因为我们的 vector 的底层是连续的数组空间,所以我们的指针就是原生的迭代器,所以我们不需要特别写一个迭代器

下面的迭代器也是比较简单的,所以这里就不过多介绍了

		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator end() const
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

10.  push_back

其实前面的函数都介绍完后介绍这个函数就简单多了,我们的 push_back 就是尾插,当然在插入之前我们是需要检查容量的,所以我们还是先检查容量,如果我们的 capacity 是0 的话,那么我们就先少给一点空间,给4个空间(其实这里给多少都可以),然后扩容结束后我们就插入数据就可以了

		void push_back(const T& value)
		{
			//判断是否需要扩容
			if (_finish == _endofstorage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			*_finish = value;
			_finish++;
		}

11. insert

insert 其实和我们前面介绍 string 时候的 insert 的逻辑差不多,也是一样的检查容量,然后挪动数据,然后插入,但是insert 是有一个问题的,我们下面会把他说清楚

		iterator insert(iterator pos, const T& value)
		{
			// check pos 是否合法
			assert(pos >= _start && pos <= _finish);

			// 判断是否需要扩容
			if (_finish == _endofstorage)
			{
				// 如果扩容,那么可能会导致 pos 位置失效(迭代器失效),所以扩容我们就要跟新 pos 的位置
				size_t len = pos - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
				// update pos
				pos = _start + len;
			}

			// 挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			// 插入数据
			*pos = value;
			++_finish;

			return pos;
		}

我们想一下,如果我们在 pos 位置插入,然后我们在插入的时候刚好引起了扩容,那么我们会怎么样?

我们引起了扩容,首先我们当然是进行扩容,这里是没有任何问题的,但是我们在扩容后还是需要插入数据的在 pos 位置,那么我们的还可以在 pos 位置插入吗?

显然是不可以的,因为我们扩容后我们的 pos 位置就失效了,所以我们如果发生了扩容后还是需要对传入的 pos 位置需要进行修改了,但是我们只能修改里面的值,不能修改外面的值,所以我们的pos 位置就是在插入后很有可能会失效的,所以我们这款i就不建议在词使用 pos 位置,但是我们的库里面是有一个解决方案的,那就是返回值,我们会返回插入后的位置,然后我们可以通过使用 pos 来接受返回值,但是我们这里还是并不建议使用 插入后的 pos 位置

12. erase

erase检查删除的 pos 位置是否合理,如果合理的话那么就删除,否则就报错,如果合理的话那么就挪动数据就好了,但是这里也有和 insert 同样的问题

		iterator erase(iterator pos)
		{
			// 检查 pos 位置合法性
			assert(pos >= _start && pos < _finish);
			// 挪动数据
			iterator current = pos + 1;
			while (current != end())
			{
				*(current - 1) = *current;
				++current;
			}
			--_finish;

			return pos;
		}

我们的现在假设我们要删除 pos 位置,如果我们删除后那么后面的元素当然会向前挪动覆盖被删除的元素,实际上如果我们不做处理,我们也是可以继续访问的,但是这里在 windows 下的 vs 下面是会报错的,这个在 linux 的 g++ 下面据没有事,这个也和平台有关,但是我们的这个 erase 的实现斌没有规定说不可以继续使用之前被使用过的迭代器,但是我们的 vs 做了特殊的处理,如果使用的话就会报错,这里我们还是建议不要使用删除后的pos 位置,因为我们不免会有一些平台事删除后会缩容,那么我们这时使用 pos 位置就会产生灾难性的后果了

13. operator[]

我们的vector可以重载方括号([ ])而且我们重载方括号也并不会麻烦,我们下面直接看代码

		T& operator[](size_t pos)
		{
			assert(_start + pos >= _start && _start + pos < _finish);

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

			return _start[pos];
		}

今天我们要讲的就是这些,这里不免会有一些缺漏,但是我还是尽量向把他说明白,如果有什么发现缺漏或者错误的地方可以联系我。

最后这个 vector 的模拟实现的代码在我的 码云 里面,有需要的可以自取

你可能感兴趣的:(C++,c++,开发语言)