【C++】-vector的模拟实现(以及memcpy如何使用)

【C++】-vector的模拟实现(以及memcpy如何使用)_第1张图片
作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:gitee✨
作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法

如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、源码理解
  • 二、模拟实现
    • 2.1成员属性
    • 2.2计算大小相关的函数
    • 2.3resize和reserve函数
    • 2.4operator[]函数
    • 2.5insert和erase函数
    • 2.6push_back和pop_back函数
    • 2.7构造函数和析构函数
    • 2.8swap函数
    • 2.9赋值运算符函数
  • 三、全部代码
  • 四、总结


前言

今天来带大家看看vector的模拟实现,思路来说还是比较简单的,vector本身也是一个动态可变的顺序表,常见功能大家还是熟悉的,接下来我先通过源码来带大家看看,然后再进行模拟实现,这样大家就会理解很多。


一、源码理解

我们看一个类首先看他的属性再看成员函数:
【C++】-vector的模拟实现(以及memcpy如何使用)_第2张图片
首先我圈出来的部分大家还是可以看懂的吧,我们vector不是像顺序表一样的通过size,capacity的这种方式来访问大小和容量了,而是通过两个指针与起始位置的差值来访问大小和容量了,为了适应任意类型,采取模板的形式,也将迭代器类型重命名出来了,我们来看看书上怎么说的

【C++】-vector的模拟实现(以及memcpy如何使用)_第3张图片

【C++】-vector的模拟实现(以及memcpy如何使用)_第4张图片

接下来我们看看他的构造函数
【C++】-vector的模拟实现(以及memcpy如何使用)_第5张图片
也是有四个,到时候模拟实现也是按照这四个进行模拟的,还有一个析构函数

计算大小和容量的一些函数
【C++】-vector的模拟实现(以及memcpy如何使用)_第6张图片
改变有效大小和容量函数:
【C++】-vector的模拟实现(以及memcpy如何使用)_第7张图片
插入删除函数:
【C++】-vector的模拟实现(以及memcpy如何使用)_第8张图片

大致就这些函数,大家大致框架应该可以看懂,里面涉及到一些空间配置器的问题(内存池),大家目前不需要理解,我们模拟实现的是使用new就行了,接下来我们来看看模拟实现吧

二、模拟实现

根据源码我们需要把源码定义出来,为了和库里面的vector发生命名冲突,我将模拟实现的vector放在一个命名空间域中,再定义两个文件:(模板的定义和声明不能分离)
【C++】-vector的模拟实现(以及memcpy如何使用)_第9张图片

2.1成员属性

#include<iostream>
#include<vector>
using namespace std;
#include<assert.h>
#include<string.h>

namespace xdh
{	
	template <class T>
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
	private:
		iterator start=nullptr;//指向目前使用空间的开头
		iterator finish=nullptr;//指向目前使用空间的结尾
		iterator end_of_storage=nullptr;//指向目前可用空间的尾
	};
}

【C++】-vector的模拟实现(以及memcpy如何使用)_第10张图片

2.2计算大小相关的函数

iterator begin() { return start; }
iterator end() { return finish; }
const_iterator cbegin()const { return start; }
const_iterator cend() const { return finish; }
size_t size()const { return finish - start; }
size_t capacity()const { return end_of_storage - start; }
bool empty() { return begin() == end(); }

这些函数大家一眼就能看懂,我就不做具体讲解了

2.3resize和reserve函数

这两个函数要先实现reserve函数,因为resize函数可能会涉及到扩容,要复用reserve函数

	void reserve(size_t n)
		{
			if (n > capacity())//其他情况也要扩容,所以要检查
			{
				size_t oldsize = size();//记录原数组有效位置到其实位置的距离
				T* tmp = new T[n];
				if (start)//原数组不为空,将原数组的数据拷贝到新数组上
				{
					//思考:使用memcpy(tmp,start,sizeof(T)*size())可不可以
					for(size_t i = 0; i < oldsize; i++)
					{
						tmp[i] = start[i];
					}
					delete[] start;//将原来的空间先进行释放

				}
				start = tmp;//改变其实位置
				finish = start + oldsize;//改变有效位置
				end_of_storage = start + n;//改变最终位置
			}
		}

思考:使用memcpy(tmp,start,sizeof(T)*size())可不可以

	void resize(size_t n, const T& val = T())
		{
			if (n <= size())//改变的大小小于原数组的的大小,直接改变有效位置的指针
			{
				finish = start + n;
				return;
			}
			if (n > capacity())//先检查是否需要扩容
			{
				reserve(n);
				iterator it = finish;
				finish = start + n;
				while (it != finish)
				{
					*it = val;//将默认值放在数组的后面
					it++;
				}
			}
		}

我们看到resize需要提供默认值,方便初始化, 我们使用匿名对象,这样初始化就是调用T类型的默认构造,给T()进行初始化,然后通过T类型的赋值运算符给val赋值,就达到初始化任务了,这里面有两个注意的点,我们的自定义类型T必须要有自己的默认构造,可以帮助自己完成默认初始化,而且还必须有默认的赋值运算,涉及到深拷贝问题,赋值运算符还要自己实现才行。

我们是内置类型就会出现const int& val=int();这种情况,C++对于这块给我们做了优化
int a=int() 加相当于int a=0;
int a=int(1) 加相当于int a=1;
int a=int(2) 加相当于int a=3;
其余内置类型也是一样的道理

2.4operator[]函数

我们有两种,一种是const的,一种是非const的,因为我们也重命名了两种迭代器

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

2.5insert和erase函数

这两个函数可以说是vector比较常用的函数了,也是比较关键的函数,我们再vector使用的时候介绍了有三种insert和两种erase,博主都给实现了一遍:

		iterator insert(iterator position, const T& val)
		{
			assert(position <= finish);//检查下标合理性
			if (finish == end_of_storage)//扩容
			{
				size_t n = position-start;//记录插入位置和起始位置的距离,到时候再新数组好定位到position
				size_t newcapacity= capacity() == 0 ? 4 : capacity() * 2;//一开始为空就扩容4个,不为空就扩容两倍
				reserve(newcapacity);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n;
			}
			iterator end = finish-1;//finish是执行那个数组的下一个位置,所以减1指向最后一个数组
			while (end >=position)
			{
				*(end + 1) = *end;//一个一个往后面移,把position位置空出来,给要插入的元素
				--end;
			}
			*position = val;
			++finish;//最终插入了一个位置,finish加1就可以了
			return position;//返回迭代器的值,防止迭代器失效
		}

		iterator insert(iterator position, size_t n, const T& val)
		{
			assert(position <= finish);
			if (finish == end_of_storage)
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish-1;
			while (end >= position)
			{
				*(end + n) = *end;//一次往往后面移n个位置,从position位置往后空出来n个位置
				end--;
			}
			finish += n;
			while (n--)//再把插入的元素插进来
			{
				*(++end) = val;
			}
			return position;
		}

		template <class Input>
		iterator insert(iterator position, Input first, Input last)
		{
			assert(position <= finish);
			int n = last - first;
			if (size() == capacity())
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n+1);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish - 1;
			while (end >= position)
			{
				*(end + n) = *end;
				end--;
			}
			finish +=n;
			while (first<last)
			{
				*(position++) = (*first++);
			}
			return position;
		}
iterator erase(iterator position)
		{
			assert(position <= finish);//检查下标合法性
			iterator begin = position;
			while (begin < finish)
			{
				*(begin) = *(begin+1);//从前往后覆盖
				begin++;
			}
			--finish;
			return position;
		}
		iterator erase(iterator first, iterator last)
		{

			assert(first<= finish);//检查合法性
			int n = last - first+1;
			
			iterator begin = first + n;
			while (begin != finish)
			{
				*(begin - n) = *begin;
				begin++;
			}
			finish-=n;
			return first;
		}

注释我已经标识好了,大家不理解的画个图理解一下:对于为什么要使用返回值,就是要考虑迭代器可能会失效,再vector的使用已经讲解过了,可以画两块空间的图,一个新空间,一个就空间,看看再扩容的时候,此时的迭代器指向那个位置。就可以理解了。

2.6push_back和pop_back函数

这是一个尾插和尾删的函数,我们之前实现了insert和erase函数,之间复用就好了

	void push_back(const T& val)
		{
			insert(end(), val);
		}
		void pop_back()
		{
			erase(end());
		}

2.7构造函数和析构函数

我为什么要把构造函数放在后面呢??原因就是我们要实现四个构造函数,有两个构造函数要复用前面的插入函数,每个构造函数的特点之前介绍过,这里就不做过多解释了

		//1.默认构造器
		vector()
			:start(nullptr)
			,finish(nullptr)
			,end_of_storage(nullptr)
		{
		}

		//2.传个数的初始化
		vector(size_t n, const T& val = T())
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}

		template <class InputIterator>
		//3.传其他迭代器进来初始化
		vector(InputIterator first, InputIterator last)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			int n = last - first;
			reserve(n);
			while (n--)
			{
				push_back(*(first++));
			}
		}

		//4.传vector进来构造(拷贝构造)
		vector(const vector<T>& x)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			size_t n = x.size();
			T* tmp = new T[n];
			for (int i = 0; i < n; i++)
			{
				tmp[i] = x[i];
			}
			start = tmp;
			finish = end_of_storage=start+n;
		}
~vector()
		{
			delete[] start;
			start = finish = end_of_storage = nullptr;
		}

我们发现我们写的所有构造里面都有初始化列表,而且都使用的是默认值nullptr,为什么要进行默认值呢,原因是再介绍构造函数那篇博客中介绍过,不写有的编译器不做处理,那么里面就是随机值,再C++11中打了这个补丁,我们再成员变量的时候就给缺省值就可以了,上面这些初始化列表都可以不用写。

我们来看一下第二个构造函数和第三个构造函数
【C++】-vector的模拟实现(以及memcpy如何使用)_第11张图片
我们看到我是想通过第二个构造函数进行初始化,但是看到报错信息和第三个构造函数报错了,这是为什么?很简单,我们的(10,2)这个参数再匹配模板的时候都会默认解释成int,说明第三个构造函数和它最匹配,上面图解也解释了,你的模板被解释成int了,而第二个构造函数再进行匹配的时候,第一个参数解释成size_t,第二个参数解释成int,显然没有第三个构造函数全部解释成int好,所有优先匹配了第三个构造函数,因为里面有解引用,所以出现了对非法地址的间接寻址
有两种解决办法
1.再传参的时候将类型规定好(10u,2)因为第三个构造函数只有一个模板,不可能解释成两个不太类型,这样只能匹配到第二个构造函数
2.将第二个构造函数的size_t改成int就可以,读者下来可以自己去看看

注意:这种情况再insert的第二个和第三个函数也出现过,我没有做修改,希望大家可以回过头来看看。

2.8swap函数

实现两个vector之间的交换,只需要交换三个属性就可以了

void swap(vector<T>& v)
		{
			std::swap(start, v.start);
			std::swap(finish, v.finish);
			std::swap(end_of_storage, v.end_of_storage);

		}

2.9赋值运算符函数

	vector<T>& operator=(vector<T>& x)
		{
			vector<T>tmp(x);
			swap(tmp);
			return *this;
		}

		vector<T>& operator=(vector<T> x)
		{
			swap(x);
			return *this;
		}

这两种都可以,我们按照原始的办法就要开辟空间进行拷贝,但我们发现这个方法拷贝构造已经帮我们实现了,第一种就直接通过拷贝构造来创建一个tmp对象,然后交换就可以了,第二种是再传参的时候就会调用拷贝构造,函数体里面之间交换就可以了,反正改变不了外面的实参


至此我们的模拟实现就到这里结束了,来解决一下刚才的思考:我们再reserve函数里面进行memcpy(tmp,start,sizeof(T)*size()),将原数组的值赋值到新数组上可以不可以??我们来看事例:
【C++】-vector的模拟实现(以及memcpy如何使用)_第12张图片
我们发现传自定义类型的参数,程序就报错了了,我只把赋值换成了memcpy函数,说明肯定是memcpy出现了错误,我们来看看为什么会出现错误:
【C++】-vector的模拟实现(以及memcpy如何使用)_第13张图片

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

此时再理解这句话是不是容易多了,对于内置类型或者自定义类型中没有涉及到空间问题,那么使用memcpy和赋值没有任何区别,涉及到空间问题,就是有可能指向释放的空间,导致出错,所以大家使用memcpy的使用要注意有没有刚才说的情况。这样才不会出现

三、全部代码

vector.h

#pragma once
#include<iostream>
#include<vector>
using namespace std;
#include<assert.h>
#include<string.h>

namespace xdh
{
	template <class T>
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin() { return start; }
		iterator end() { return finish; }
		const_iterator cbegin()const { return start; }
		const_iterator cend() const { return finish; }
		size_t size()const { return finish - start; }
		size_t capacity()const { return end_of_storage - start; }
		bool empty() { return begin() == end(); }

		//==============reserse函数的实现=================
		void reserve(size_t n)
		{
			if (n > capacity())//其他情况也要扩容,所以要检查
			{
				size_t oldsize = size();//记录原数组有效位置到其实位置的距离
				T* tmp = new T[n];
				
				if (start)//原数组不为空,将原数组的数据拷贝到新数组上
				{
					for(size_t i = 0; i < oldsize; i++)
					{
						tmp[i] = start[i];
					}
					//memcpy(tmp, start, sizeof(T) * oldsize);
					delete[] start;//将原来的空间先进行释放
					
				}
				start = tmp;//改变其实位置
				finish = start + oldsize;//改变有效位置
				end_of_storage = start + n;//改变最终位置
			}
		}

		//============resize函数的实现=============
		void resize(size_t n, const T& val = T())
		{
			if (n <= size())//改变的大小小于原数组的的大小,直接改变有效位置的指针
			{
				finish = start + n;
				return;
			}
			if (n > capacity())//先检查是否需要扩容
			{
				reserve(n);
				iterator it = finish;
				finish = start + n;
				while (it != finish)
				{
					*it = val;//将默认值放在数组的后面
					it++;
				}
			}
		}
		//==============构造器============
		//1.默认构造器
		vector()
			:start(nullptr)
			,finish(nullptr)
			,end_of_storage(nullptr)
		{
		}

		//2.传个数的初始化
		vector(size_t n, const T& val = T())
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}

		template <class InputIterator>
		//3.传其他迭代器进来初始化
		vector(InputIterator first, InputIterator last)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			int n = last - first;
			reserve(n);
			while (n--)
			{
				push_back(*(first++));
			}
		}

		//4.传vector进来构造(拷贝构造)
		vector(const vector<T>& x)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			size_t n = x.size();
			T* tmp = new T[n];
			for (int i = 0; i < n; i++)
			{
				tmp[i] = x[i];
			}
			start = tmp;
			finish = end_of_storage=start+n;
		}

		//============operator=函数的实现==========
		vector<T>& operator=(vector<T>& x)
		{
			vector<T>tmp(x);
			swap(tmp);
			return *this;
		}

		vector<T>& operator=(vector<T> x)
		{
			swap(x);
			return *this;
		}
		//============析构函数=========
		~vector()
		{
			delete[] start;
			start = finish = end_of_storage = nullptr;
		}
		//===========operator[]函数的实现==========
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return start[pos];
		}
		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return start[pos];
		}


		//==============insert函数的实现============
		iterator insert(iterator position, const T& val)
		{
			assert(position <= finish);//检查下标合理性
			if (finish == end_of_storage)//扩容
			{
				size_t n = position-start;//记录插入位置和起始位置的距离,到时候再新数组好定位到position
				size_t newcapacity= capacity() == 0 ? 4 : capacity() * 2;//一开始为空就扩容4个,不为空就扩容两倍
				reserve(newcapacity);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n;
			}
			iterator end = finish-1;//finish是执行那个数组的下一个位置,所以减1指向最后一个数组
			while (end >=position)
			{
				*(end + 1) = *end;//一个一个往后面移,把position位置空出来,给要插入的元素
				--end;
			}
			*position = val;
			++finish;//最终插入了一个位置,finish加1就可以了
			return position;//返回迭代器的值,防止迭代器失效
		}
		void insert(iterator position, int n, const T& val)
		{
			assert(position <= finish);
			if (finish == end_of_storage)
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish-1;
			while (end >= position)
			{
				*(end + n) = *end;//一次往往后面移n个位置,从position位置往后空出来n个位置
				end--;
			}
			finish += n;
			while (n--)//再把插入的元素插进来
			{
				*(++end) = val;
			}
			
		}

		template <class Input>
		void insert(iterator position, Input first, Input last)
		{
			assert(position <= finish);
			int n = last - first;
			if (size() == capacity())
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n+1);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish - 1;
			while (end >= position)
			{
				*(end + n) = *end;
				end--;
			}
			finish += n;
			while (first<last)
			{
				*(position++) = (*first++);
			}
			
		}


		//==============erase的函数实现===============
		iterator erase(iterator position)
		{
			assert(position <= finish);//检查下标合法性
			iterator begin = position;
			while (begin < finish)
			{
				*(begin) = *(begin+1);//从前往后覆盖
				begin++;
			}
			--finish;
			return position;
		}
		iterator erase(iterator first, iterator last)
		{

			assert(first<= finish);//检查合法性
			int n = last - first+1;
			
			iterator begin = first + n;
			while (begin != finish)
			{
				*(begin - n) = *begin;
				begin++;
			}
			finish-=n;
			return first;
		}


		//==============push_back和pop_back函数的实现=============
		void push_back(const T& val)
		{
			insert(end(), val);
		}
		void pop_back()
		{
			erase(end());
		}


		//==============swap函数的实现====================
		void swap(vector<T>& v)
		{
			std::swap(start, v.start);
			std::swap(finish, v.finish);
			std::swap(end_of_storage, v.end_of_storage);

		}
	private:
		iterator start;//指向目前使用空间的开头
		iterator finish;//指向目前使用空间的结尾
		iterator end_of_storage;//指向目前可用空间的尾
	};
}

四、总结

我们的vector模拟实现总算讲完了,里面要注意的细节还是非常多,所以说C++没学好,里面的坑还是非常多的,博主自己再实现的时候也出现了很多错误,一定要写一个功能旧测试一个,不然出错了就很痛苦,那我们这篇就说到这里,下篇我们开始介绍list,这个容器难度就上来了,因为涉及到带头双向循环链表的结构,不用担心,跟着博主走,没有难的事。【C++】-vector的模拟实现(以及memcpy如何使用)_第14张图片

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