【C++】STL基础必备:深入解析vector容器的实现(含源码)

文章目录

一、vector的结构

vector的底层本质上是顺序表,但是跟我们之前的实现略微有些不同,最大的不同就是结构的不同,我们翻看stl源码中的stl_vector,我们来看看它的结构是如何定义的,如果没有stl源码可以私信我,如下图:

我们看到它有四个成员,第一个成员我们不管,我们就看框出来的三个成员,这三个成员变量的类型居然是一致的,都是iterator,我们又来看看这个iterator是什么,如图:

从上面图中看来,iterator其实就是T*,也就是数据类型的指针,使用了模板,而不像string已经指定了数据类型为char,而且这里的iterator也是vector的迭代器,是指针,为了方便大家观察,我把上面的话总结一下写成结构,如下:

cpp 复制代码
//iterator就是T*
T* start;
T* finish;
T* end_of_storage;

那么现在我们先根据stl源码中的结构写出我们自己vector的结构,随后我再给大家一一解释它们的含义,如下:

cpp 复制代码
#include <iostream>
#include <cassert>
using namespace std;

namespace TL
{
	template<class T>
	class vector
	{
	public:
		
	private:
		T* _start = nullptr;
		T* _finish = nullptr;
		T* _endofstorage = nullptr; 
	};
}

其中start就是数组的起始地址,finish就是数组最后一个元素的下一个元素的地址,endofstorage是容量的最后一个元素的下一个元素的地址,如图:

它们分别指向了一个数组最重要的几个部分,通过这几个指针的运算可以得到这个数据的大小以及容量,和之前结构本质上没有什么不同,各有好处,那么我们的vector就按照这种方式来实现

二、vector容器相关接口

我们来看看vector容器相关接口又哪些:

我们一个一个来实现,顺便帮大家熟悉熟悉这种结构的意义,首先是求这个数组的有效数据的个数,其实非常简单,由于finish指向最后一个元素的下一个元素,而start指向第一个元素,我们直接让这两个指针相减即可得到有效数据个数,如下:

cpp 复制代码
size_t size() const
{
	return _finish - _start;
}

随后我们就来求数组容量大小,大家可能都猜到了,就是用endofstorage减去start,因为它就指向数组最后一个位置的下一个位置,如下:

cpp 复制代码
size_t capacity() const 
{
	return _endofstorage - _start;
}

接下来我们来实现判空函数,也很简单,我们只需要看看start和finish是否相等,如果相等,说明数组是空的,如下:

cpp 复制代码
bool empty() const
{
	return _start == _finish;
}

还有两个较为复杂的接口,就是resize和reserve,这个我们在修改接口中讲

三、vector元素访问接口

我们来看看vector元素访问接口有哪些,如下:

我们还是实现前四个,实现方法与string相同,这里我就不再多解析,如下:

cpp 复制代码
T& operator[](size_t pos)
{
	assert(pos < size());
	return _start[pos];
}

T& at(size_t pos)
{
	assert(pos < size());
	return _start[pos];
}

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

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

T& front() 
{
	assert(!empty());
	return *_start;
}

T& back()
{
	assert(!empty());
	return *(_finish - 1);
}

const T& front() const
{
	assert(!empty());
	return *_start;
}

const T& back() const
{
	assert(!empty());
	return *(_finish - 1);
}

四、vector的迭代器

vector的迭代器其实和string是一样的,也是指针,因为它们的底层都是数组,可以用指针直接访问元素,不同的地方就是string类的底层数组的类型已经确定为char,而vector底层数组的类型就不确定了,因为vector是一个类模板

接下来我们就按照string类的迭代器实现方式实现一下vector的迭代器,如下:

cpp 复制代码
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;
}

五、vector的修改接口

我们来看看vector的修改接口有哪些:

我已经在图中做好了记号,框起来的那部分就是我们要实现的修改接口,当然还要加上之前在容量接口中欠的resize和reserve,其实这些接口的实现就跟string类差不多,会了string类接口的实现写这个也很轻松,但是我们还是都简单过一下

reserve

vector中reserve的实现和string类的reserve的实现不能说很像,简直是一模一样,所以这里我们直接给出,如果不懂的可以看上一篇string类的实现:

cpp 复制代码
void reserve(size_t n)
{
	size_t Size = size();
	size_t Capacity = capacity();
	if (n <= Capacity)
	{
		return;
	}

	size_t newCapacity = 2 * Capacity;
	if (newCapacity < n)
		newCapacity = n;

	T* tmp = new T[newCapacity];
	for (size_t i = 0; i < Size; i++)
	{
		tmp[i] = _start[i];
	}
	if (_start)
		delete[] _start;
	_start = tmp;
	_finish = _start + Size;
	_endofstorage = _start + newCapacity;
}

resize

resize的实现也和string类似,如下:

cpp 复制代码
void resize(size_t n, const T& x = T())
{
 	//优化前
	/*if (n < size())
	{
		_finish = _start + n;
	}
	else if(n < capacity())
	{
		for (size_t i = size(); i < n; i++)
		{
			push_back(x);
		}
	}
	else
	{
		reserve(n);
		for (size_t i = size(); i < n; i++)
		{
			push_back(x);
		}
	}*/

	//优化后
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		//如果n没有capacity大,reserve不会扩容,所以可以优化
		reserve(n);
		for (size_t i = size(); i < n; i++)
		{
			push_back(x);
		}
	}
}

swap

交换两个vector对象的方法就是分别交换它们的三个指针,如下:

cpp 复制代码
void swap(vector& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

clear

清空数据其实就是让finish重新等于start,如下:

cpp 复制代码
void clear()
{
	_finish = _start;
}

push_back与pop_back

push_back和pop_back的实现还是和string差不多,并且还不用设置\0,更加简单了,如下:

cpp 复制代码
void push_back(const T& x)
{
	if (_finish == _endofstorage)
	{
		reserve(capacity() * 2);
	}
	*_finish = x;
	_finish++;
}

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

insert

上面我们实现的大部分接口都和string类的接口差不多,但是insert和erase就稍微有点不同了, vector只能通过迭代器进行操作,但是解决的方法也很简单,因为迭代器其实就是指针,我们可以通过指针相减的方式得到下标等信息,重新转化为string类那种插入与删除的方式

但是要注意的是,vector的insert要重新返回新的迭代器,不能忘了,其它方面就和string类的insert差不多,如下:

cpp 复制代码
iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _endofstorage);
	//转化为string类的处理方式,即使用下标处理
	size_t Pos = pos - _start;
	if (_finish == _endofstorage)
	{
		reserve(2 * capacity());
	}
	size_t src = size() - 1;
	size_t dest = src + 1;
	while ((int)src >= (int)Pos)
	{
		_start[dest--] = _start[src--];
	}
	_start[Pos] = x;
	_finish++;
	//不要忘记返回,因为原本的start可能会被修改
	return _start + Pos;
}

iterator insert(iterator pos, size_t n, const T& x)
{
	assert(pos >= _start && pos <= _endofstorage);
	size_t Pos = pos - _start;
	if (size() + n > capacity())
	{
		reserve(size() + n);
	}
	size_t src = size() - 1;
	size_t dest = src + n;
	while ((int)src >= (int)Pos)
	{
		_start[dest--] = _start[src--];
	}
	for (size_t i = Pos; i < Pos + n; i++)
	{
		_start[i] = x;
	}
	_finish += n;
	return _start + Pos;
}

erase

上面的insert我们就是将有关迭代器的问题转化为了下标问题,erase也可以按照insert的思路写,但是为了让大家体会到不同的实现方式,下面我们就直接用迭代器的方式来完成删除操作,如下:

cpp 复制代码
iterator erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);
	iterator it = pos + 1;
	while (it < end())
	{
		//将数据往前挪动
		*(it - 1) = *it;
	}
	
	_finish--;
	return pos;
}

六、vector的默认成员函数重载

接下来我们继续来介绍vector的默认成员函数重载,它们也和string类似,但是有些许不同,如果掌握了string类的实现,下面的实现基本上也没有问题

构造函数

在构造函数部分我们会讲两个接口,一个就是普通的n个value的默认构造,一个就是用大括号初始化的默认构造,我们先来讲第一个比较简单的接口

我们要实现n个value的构造,那么我们就要预先开辟n个空间,然后写个循环将这个值尾插进入vector即可,如下:

cpp 复制代码
//T()是调用对应类型的默认构造
vector(size_t n = 0, const T& x = T())
{
	//start和endofstorage将会在reserve中被修改
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		//push_back中会修改finish
		push_back(x);
	}
}

接下来我们就要来实现用大括号初始化vector对象,主要是我们要认识一下initializer_list,它是一个C++11标准提出的容器,拥有类似于数组的结构,它的两个成员变量是两个指针,分别指向这个数组的首和尾

这个容器就是为了拿来支持大括号初始化的,编译器会根据情况用大括号中的元素构造这个容器的对象,随后将这个对象传到构造函数中,并且这个容器也支持了迭代器,我们可以来看看:

所以我们接收到了initializer_list对象之后,可以使用迭代器遍历它,然后将其中取到的元素一一尾插到vector对象中即可,有关initializer_list的更多解析将会在以后的C++11标准介绍中进行,那么这个版本的构造函数实现如下:

cpp 复制代码
vector(const initializer_list<T>& il)
{
	//提前开空间提升性能
	reserve(il.size());
	for (auto& e : il)
	{
		//将其中的元素一一尾插到vector对象中
		push_back(e);
	}
}

拷贝构造函数

拷贝构造函数就是开空间,然后遍历目标vector对象,将其中的元素一一插入,如下:

cpp 复制代码
vector(const vector& v)
{
	//提前预留空间
	reserve(v.capacity());
	for (auto& e : v)
	{
		//将目标vector对象的元素一一取出插入
		push_back(e);
	}
}

赋值重载函数

赋值重载我们可以利用之前实现的swap函数,跟string那里的思路一致,如下:

cpp 复制代码
vector& operator=(vector v)
{
	//确保不是给自己赋值
	if (this != &v)
	{
		//和string差不多,利用swap带走资源
		swap(v);
	}
	return *this;
}

析构函数

析构函数就更简单了,直接释放对应的数组即可,如下:

cpp 复制代码
~vector()
{
	if (_start)
		delete[] _start;
	_start = _finish = _endofstorage = nullptr;
}

七、源码

cpp 复制代码
#pragma once

#include <iostream>
#include <cassert>
using namespace std;

namespace TL
{
	template<class T>
	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;
		}

		vector(size_t n = 0, const T& x = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(x);
			}
		}

		vector(const initializer_list<T>& il)
		{
			reserve(il.size());
			for (auto e : il)
			{
				push_back(e);
			}
		}
		
		void swap(vector& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

	/*	vector(const vector& v)
		{
			vector tmp;
			tmp.reserve(v.capacity());
			for (size_t i = 0; i < v.size(); i++)
			{
				tmp.push_back(v[i]);
			}
			swap(tmp);
		}*/

		vector(const vector& v)
		{
			//提前预留空间
			reserve(v.capacity());
			for (auto& e : v)
			{
				//将目标vector对象的元素一一取出插入
				push_back(e);
			}
		}

		vector& operator=(vector v)
		{
			if (this != &v)
			{
				//和string差不多,利用swap带走资源
				swap(v);
			}
			return *this;
		}

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

		void reserve(size_t n)
		{
			size_t Size = size();
			size_t Capacity = capacity();
			if (n <= Capacity)
			{
				return;
			}

			size_t newCapacity = 2 * Capacity;
			if (newCapacity < n)
				newCapacity = n;

			T* tmp = new T[newCapacity];
			for (size_t i = 0; i < Size; i++)
			{
				tmp[i] = _start[i];
			}
			if (_start)
				delete[] _start;
			_start = tmp;
			_finish = _start + Size;
			_endofstorage = _start + newCapacity;
		}

		void resize(size_t n, const T& x = T())
		{
			/*if (n < size())
			{
				_finish = _start + n;
			}
			else if(n < capacity())
			{
				for (size_t i = size(); i < n; i++)
				{
					push_back(x);
				}
			}
			else
			{
				reserve(n);
				for (size_t i = size(); i < n; i++)
				{
					push_back(x);
				}
			}*/

			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				for (size_t i = size(); i < n; i++)
				{
					push_back(x);
				}
			}
		}

		void push_back(const T& x)
		{
			if (_finish == _endofstorage)
			{
				reserve(capacity() * 2);
			}
			*_finish = x;
			_finish++;
		}

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

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _endofstorage);
			size_t Pos = pos - _start;
			if (_finish == _endofstorage)
			{
				reserve(2 * capacity());
			}
			size_t src = size() - 1;
			size_t dest = src + 1;
			while ((int)src >= (int)Pos)
			{
				_start[dest--] = _start[src--];
			}
			_start[Pos] = x;
			_finish++;
			return _start + Pos;
		}

		iterator insert(iterator pos, size_t n, const T& x)
		{
			assert(pos >= _start && pos <= _endofstorage);
			size_t Pos = pos - _start;
			if (size() + n > capacity())
			{
				reserve(size() + n);
			}
			size_t src = size() - 1;
			size_t dest = src + n;
			while ((int)src >= (int)Pos)
			{
				_start[dest--] = _start[src--];
			}
			for (size_t i = Pos; i < Pos + n; i++)
			{
				_start[i] = x;
			}
			_finish += n;
			return _start + Pos;
		}

		/*iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);
			size_t Pos = pos - _start;
			size_t src = Pos + 1;
			size_t dest = Pos;
			while (src < size())
			{
				_start[dest++] = _start[src++];
			}
			_finish--;
			return pos;
		}*/

		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);
			
			iterator it = pos + 1;
			while (it < end())
			{
				*(it - 1) = *it;
			}
			
			_finish--;
			return pos;
		}

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

		T& at(size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

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

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

		T& front() 
		{
			assert(!empty());
			return *_start;
		}

		T& back()
		{
			assert(!empty());
			return *(_finish - 1);
		}

		const T& front() const
		{
			assert(!empty());
			return *_start;
		}

		const T& back() const
		{
			assert(!empty());
			return *(_finish - 1);
		}

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

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

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

		void clear()
		{
			_finish = _start;
		}


			


	private:
		T* _start = nullptr;
		T* _finish = nullptr;
		T* _endofstorage = nullptr; 
	};

	template<class T>
	void swap(vector<T>& v1, vector<T>& v2)
	{
		v1.swap(v2);
	}
}

那么今天关于vector的实现就讲到这里,有什么不懂欢迎私信问我,我会及时做出解答,下一篇文章开始我们学习list的使用,敬请期待吧!

bye~

相关推荐
paeamecium1 小时前
【PAT甲级真题】- Spell It Right(20)
c++·pat考试·pat
羊群智妍1 小时前
2026年AI搜索优化监测工具|免费好用的GEO工具推荐
笔记
2601_954035051 小时前
心情日记撰写就用一个APP就够了
笔记
慢慢向上的蜗牛1 小时前
Atlas300I推理卡驱动适配Linux 6.12+内核
linux·c++·人工智能·华为·驱动·底层开发·ascend
50万马克的面包1 小时前
C语言第3讲:分支和循环
c语言·开发语言·笔记·算法
ytttr8731 小时前
惯性导航精解算程序(MATLAB实现)
开发语言·matlab
艺杯羹1 小时前
从零搭建CSDN博客爬虫:Python爬虫+多格式导出完整教程
开发语言·爬虫·python·开源·gui·csdn
码农小韩1 小时前
QT学习记录(三)——C++学习基础(三)
开发语言·c++·qt·学习·算法·嵌入式软件