vector的使用和模拟实现

有了前面的经验,这里对于vector的使用我们就简单看一眼:

cpp 复制代码
void test_vector()
{
	vector<int>v1;
	vector<int>v2(10, 1);//初始化 v2 里面有10个1

	vector<int>v3(v2.begin(), v2.end());//用一段初始化区间来初始化


	for (size_t i = 0; i < v3.size(); i++)
		cout << v3[i] << " ";


	vector<int>::iterator it = v3.begin();
	while (it != v3.end())
	{
		cout << *it << " ";
		it++;
	}

	for (auto e : v3)
	{
		cout << e << " ";
	}
}

vector中我们同样可以使用reserve来为数组预留空间。但vectorstring多出了一个resize的操作。

cpp 复制代码
void test_vector()
{
	vector<int>v;
	v.reserve(100);//预留100个空间,至少开足够v的内容,所以可能多一些,具体实现看编译器,但不会对内容产生影响。VS下不会缩容

	v.resize(200, 2);//将v的size改为200,如果原来的size大于,则删除后面的数据,
	                 //如果小于,先看是否有给出数据,有的话按给出的数据(2)补齐,如果没有,则按默认构造补齐数据
	                 //当容量不够的时候可能会扩容但不会缩容
}

vector的插入元素主要依赖push_backinsert两种操作,但值得注意的是,insert操作只支持迭代器操作,同理,erase操作同样也只支持迭代器操作。

cpp 复制代码
void test_vector()
{
	vector<int> v(10, 1);
	v.push_back(2);
	v.insert(v.begin(), 0);//intsert只支持迭代器操作:在开头位置插入一个0

	v.erase(v.begin());//删除操作,也只支持迭代器操纵
}

我们可以使用vector模拟实现出一个string,但两者存在一个重要的区别:string中存在\0,但vector中不存在。

vector的模拟实现:

下面我们来到今天的重头戏,vector的模拟实现:

这里我们用模板类型的指针来代替迭代器,在写一个类模板之前,首先我们需要实现函数的默认构造和析构函数,防止在具体使用的时候出错导致程序崩溃。

cpp 复制代码
namespace vector
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//C++11 强制生成默认构造
		vector() = default;

		vector(const vector<T>& v)
		{
			reserve(v.size());
			for (auto& e : v)
			{
				push_back(e);
			}
		}
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}
		
		// 类模板的成员函数,还可以继续是函数模板
		// 写成模板的价值:任意容器迭代器初始化,但类型必须是匹配的
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
}

接着来实现容器的核心功能:迭代器。这是后续其他函数书写和使用的基础。从上文中typedef中可以看出,迭代器同样分为正常迭代器和const迭代器,以便能够实现不同的任务需要。

cpp 复制代码
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}

在完成初始化以及迭代器的实现之后,我们可以利用他们去实现一些vector中的基础函数:

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

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

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

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

void insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);

	//扩容
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}

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

	++_finish;
}

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		//memcpy(tmp, _start, size() * sizeof(T));//memcpy浅拷贝,这里拷贝的是指针指向的空间中的东西,按字节拷贝,
		//  里面的东西依然是指向被拷贝对象的指针,后面delete之后会出错
		//  需要我们自己实现一个深拷贝
		for (size_t i = 0; i < old_size; i++)
		{
			tmp[i] = _start[i];//调用赋值
		}
		
		delete[] _start;
		_finish = tmp + old_size;
		_start = tmp;
		_end_of_storage = _start + n;
	}
}

void resize(size_t n, T val = T())
{
	if (n < size())
		_finish = _start + n;
	else
	{
		reserve(n);
		while (_finish < _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
}

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

void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	*_finish = x;
	++_finish;
}

这里涉及到的内容都比较简单且在之前的文章中基本上都讲解过原理了,这里就不再过多赘述了。

在上面的函数实现的过程中,我们用到了[],但在此之前我们还没有实现对其的重载和实现,因此理论上来讲上面的函数实现是不能用的,那么现在我们再来模拟试下一些对[]的重载实现:

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

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

这里还有一个比较有意思的函数重载:=的重载,这里容笔者稍微卖弄一下,分为传统写法和现代写法两种写法,正常来讲选择自己最熟悉的就可以了。

传统写法:

cpp 复制代码
void clear()
{
	_finish = _start;
}
vector<T>& operator=(const vector<T>& v)
{
	if (this != &v)
	{
		clear();
		reserve(v.size());

		for (auto& e : v)
		{
			push_back(e);
		}
		
	}
	return *this;
}

现代写法:

cpp 复制代码
void swap()
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

最后,在日常使用中,我们可能会遇到迭代器失效的问题,这个问题分两种情况:

  1. 扩容导致的迭代器失效:迭代器失效的本质是指向的内存空间被释放了,变成了野指针
  2. 非扩容导致的迭代器失效:由于数据挪动,迭代器指向的元素改变了,此时迭代器也失效了,不要访问

好了,本章的内容就到此结束,感谢阅读!

相关推荐
断春风几秒前
从 JDK 8 到 JDK 21:企业级 Java 版本选择的架构思考
java·架构·jdk
h7ml2 分钟前
构建可扩展的企业微信消息推送服务:事件驱动架构在Java中的应用*
java·架构·企业微信
前端小L2 分钟前
贪心算法专题(十五):借位与填充的智慧——「单调递增的数字」
javascript·算法·贪心算法
想学后端的前端工程师3 分钟前
【浏览器工作原理与性能优化指南:深入理解Web性能】
前端·性能优化
heartbeat..6 分钟前
JavaWeb 核心:HttpServletRequest 请求行、请求头、请求参数完整梳理
java·网络·web·request
程序员爱钓鱼9 分钟前
Node.js 编程实战:错误处理与安全防护
前端·后端·node.js
前端小L10 分钟前
贪心算法专题(十四):万流归宗——「合并区间」
javascript·算法·贪心算法
Geoffwo10 分钟前
Electron 打包后 exe 对应的 asar 解压 / 打包完整流程
前端·javascript·electron
柒@宝儿姐13 分钟前
vue3中使用element-plus的el-scrollbar实现自动滚动(横向/纵横滚动)
前端·javascript·vue.js
程序员爱钓鱼14 分钟前
Node.js 编程实战:模板引擎与静态资源
前端·后端·node.js