Cpp学习——vector模拟实现

news2024/11/25 4:39:22

 

vector简介

在模拟实现vector之前,首先就得知道vector是个啥?vector是个啥呢?vector是一个stl里面的容器,并且是一个模板容器。它就像是一个顺序表模板。还记得顺序表吧?之前我实现的顺序表只能弄整形的数据,但是vector却可以弄几乎所有的数据。所以要实现vector便可以参考实现顺序表的功能来实现,并且还可以结合一下stl里面的源代码。

 vector实现

1.vector的成员

在这里就得来参考一下stl里面的源代码了。看一看stl里面的vector成员是什么。源代码:

这便是vs2019下面的vector的成员,pointer便是typedef后的指针。但是这个指针可不是一般的指针,而是模板指针。来看一下:

 vs2019下面的源代码复杂了,复杂到让我们这些小白看不懂。反正就记住vector的成员是下面三个便可以了:_start,_finish,_endofstorage。分别代表的意思就是:指向vector容器的开头,容器内内容的结尾,容器容量的结尾。那我们在实现时该怎么定义呢?我们可以像下面一样去定义:

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




	private:
		iterator _start;
		iterator _finish;
		iterator _endofstrage;
};
}

1.首先搞一块area域来写自己的vector。

2.建立模板,并将模板的指针类型T* typedef为iterator,const T*定义为const_iterator。要在public的区域内。

3.再用typedef后的模板指针来定义成员。

2.vector初始化构造以及capacity(),size()函数

要使用一个对象,就得初始化一个对象。初始化对象应该用什么值呢?很明显,指针类型就得用空指针来初始化。初始化函数如下:

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

		}

接下来便是写一个尾插函数,但是这个函数是有问题的。尾插函数如下:

void push_back( const T& x)
		{
			int sz = size();
			if (_finish == _endofstorage)
			{
				int cp = capacity() == 0 ? 4 : 2 * capacity();
				T* tmp = new T[cp];
				if (_start)
				{
				memcpy(tmp, _start, sizeof(T) * size());
				delete[]_start;
			}

				_start = tmp;

				_finish = _start + sz;
				_endofstorage = _start + cp;
			}


			*_finish = x;
			++_finish;
		}

3.reserve函数与resize函数

resever函数实现的是一个扩容的操作,resize函数实现的就是一个扩容加初始化的函数了。所以按照两者的功能便可以先实现reserve函数再实现resize函数。reserve函数实现代码如下:

void reserve(size_t n)
		{
			int sz = size();
			if (capacity() <= n)
			{
				T* tmp = new T[n];

				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * size());
					delete[] _start;
				}

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

有了reserve()函数接下来实现resize()函数就简单了。代码如下:

void resize(size_t n,const T& val = T())
		{
			reserve(n);//先检查是否扩容或者是否要开空间。

			iterator it = end();//将数据尾到容量尾这一段空间的元素初始化掉
			while (it != _endofstorage)
			{
				*it = val;
				it++;
			}
		    
			_finish += n;
		}

这里值的考究的便是在数据的结尾到容器的结尾到底该放什么数据呢?这里给的缺省值是

T()。这是一个匿名构造,构造出来的值是根据实例化出来的T的默认构造的值。这样便可以避免因为给死一个缺省值而导致的类型不匹配的错误。有了reserve()函数以后便可以对push_back()函数进行改良,改良如下:

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

			*_finish = x;
			++_finish;
		}

4,insert()函数与erase()函数

这两个函数实现的便是vector的中间插入与删除的功能。但是在这两个函数实现的过程中会出现迭代器失效的问题。

1.先来实现一下insert()函数,代码如下:

void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			reserve(size());//检查扩容
			 
			iterator end = _finish;
			while (end != pos)
			{
				*end = *(end - 1);
				end--;
			}
			*pos = val;
			_finish++;
		}

但是这个代码是有一丝丝问题的。比如当我插入以下数据时:

	void test_vector3()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);//尾插五个元素

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

		cout << endl;

		v1.insert(v1.begin(), 22);//再中间插入三个元素
		v1.insert(v1.begin()+2, 66);
		v1.insert(v1.end(), 99);

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

		cout << endl;

	}

结果:

这样是可以正常的跑起来的。但是当我的数据变成:

void test_vector3()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

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

		cout << endl;

		v1.insert(v1.begin(), 22);
		v1.insert(v1.begin()+2, 66);
		v1.insert(v1.end(), 99);
		v1.insert(v1.end(), 100);
		for (auto e : v1)
		{
			cout << e << " ";
		}

		cout << endl;

	}

 也就是再中间插入一个100时便会发生错误。错误如下:

为什么呢?其实这里是发生了迭代器失效的问题,从而引发了野指针的问题。为什么会发生迭代器失效呢?原因是发生了扩容。原来我的数据是这样的:

 这个时候我就得发生扩容了吧,因为容量已经满了。我们的扩容是怎么扩的呢?是这样扩的:

T* tmp = new T[n];

if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());
delete[] _start;
	}

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

原来的数据经过扩容操作以后就会变成:

 但是我的pos是在那个位置呢?是在原来那个没有扩容的小空间上。为什么?因为我是在用begin()迭代器来完成插入操作。在调用begin()的时候,还没有扩容。所以pos的位置便在原来的那个已被销毁的空间上,所以pos变成了野指针。pos与_finish的位置关系如下图:

但是我的结束条件是这个:end!=pos,end一开始便是_finish。

iterator end = _finish;
while (end != pos)
{
  *end = *(end - 1);
  end--;
			}

 所以end与pos是在两块不同的空间上的。说以这个条件是不会结束的。所以end会一直访问一些不该访问的空间。所以发生了错误。那我们该如何去修改呢?其实很简单,在扩容之前记录一下pos与_start的偏移量,在扩容后将pos的位置更新一下便可以了。

void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			int len = pos - _start;//记录偏移量
			reserve(size()+1);//插入一个数据后容量加1
			pos = _start + len;//更行pos
			iterator end = _finish;
			while (end != pos)
			{
				*end = *(end - 1);
				end--;
			}
			*pos = val;
			_finish++;
		}

然后便可以正常运行了:

 2.erase()函数,实现代码如下:

         void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

			iterator end = pos+1;

			while (end < _finish)
			{
				*(end - 1) = *end;
				end++;
			}
			_finish--;
		}

在正常情况下:

void test_vector5()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

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

		cout << endl;

		v1.erase(v1.begin());
		v1.erase(v1.end());

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

	}

这是可以正常使用的:

但是在某些特殊的场景之下这就会发生迭代器失效的情况。比如我要将一列数据中的偶数给删除时。在以下情况下,用我写的erase代码的情况如下:

1。偶数不在尾部且偶数不连续:

void test_vector5()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		v1.push_back(6);
		v1.push_back(7);

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

		cout << endl;

	  vector<int>::	iterator it = v1.begin();

	  while (it != v1.end())
	  {
		  if (*it % 2 == 0)
		  {
			  v1.erase(it);
		  }
		  it++;
	  }

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

	}

 运行结果是正常的:

 2.偶数有连续的情况:

void test_vector5()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(2);
		v1.push_back(4);
		v1.push_back(5);
		v1.push_back(6);
		v1.push_back(7);

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

		cout << endl;

	  vector<int>::	iterator it = v1.begin();

	  while (it != v1.end())
	  {
		  if (*it % 2 == 0)
		  {
			  v1.erase(it);
		  }
		  it++;
	  }

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

	}

运行结果是有错误的:结果不对

 3.当尾部的数据是偶数时:程序崩溃

void test_vector5()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		v1.push_back(6);
		v1.push_back(7);
		v1.push_back(8);

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

		cout << endl;

	  vector<int>::	iterator it = v1.begin();

	  while (it != v1.end())
	  {
		  if (*it % 2 == 0)
		  {
			  v1.erase(it);
		  }
		  it++;
	  }

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

	}
}

结果:

这是为什么呢?当数据内有连续偶数时删不干净其实就是我们的操作逻辑写错了。当要删除元素是我们不应该++it而是应该让it保持不动,对移动后的数据还得来一次判断。所以我们要将删除逻辑改为这样:

void test_vector5()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(2);
		v1.push_back(4);
		v1.push_back(5);
		v1.push_back(6);
		v1.push_back(7);

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

		cout << endl;

	  vector<int>::	iterator it = v1.begin();

	  while (it != v1.end())
	  {
		  if (*it % 2 == 0)
		  {
			  v1.erase(it);//删除后不应该++,应改再对这个位置上的新数据进行判断
		  }
		  else
          {
            it++//不是偶数时再++
          }
	  }

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

	}

结果:

对于尾巴上是偶数的删除,结果也是对的:

这里的迭代器失效问题就是迭代器位置失去意义。

 5.深拷贝与浅拷贝问题(大坑)

先来写一个拷贝构造函数:

vector( vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			T* tmp = new T[v.capacity()];
			if (v._start)
			{
				memcpy(tmp, v._start, sizeof(T) * v.size());
			}
			_start = tmp;
			_finish = tmp + v.size();
			_endofstorage = tmp + v.capacity();
		}

来个整型试试看:

void test_vector6()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
        v1.push_back(5);

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

		cout << endl;

		vector<int>v2(v1);

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

		cout << endl;

	}

结果:

程序正常运行。

 再来一个string:

void test_vector7()
	{
		vector<string>v1;
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		//v1.push_back("11111");

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

		cout << endl;

	}

只插入四个的话,程序正常运行:

但是,当我要插入第五个元素时:

void test_vector7()
	{
		vector<string>v1;
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");

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

		cout << endl;

	}

程序崩掉了:

为啥呢?其实还是因为扩容,也就是reserve()函数。再来看看reserve()函数的实现:

void reserve(size_t n)
		{
			int sz = size();
			if (capacity() <= n)
			{
				T* tmp = new T[n];

				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * size());
					delete[] _start;
				}

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

 这里拷贝数据的方式是memcpy()。拷贝完了以后就要将_start里的数据给释放掉。但是memcpy函数实现的是浅拷贝,而string对象里面可是有指针的。这样的话,在释放掉原来的空间后再调用析构函数就会对同一块空间进行两次释放导致报错。该怎么改呢?调用深拷贝就行了。改进如下:

void reserve(size_t n)
		{
			int sz = size();
			if (capacity() <= n)
			{
				T* tmp = new T[n];

				if (_start)
				{
					for (int i = 0;i < size();i++)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}

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

为什么这样就可以了呢?这是因为自定义类型的=实现的就是深拷贝。

再来调用vector的拷贝构造:

void test_vector7()
	{
		vector<string>v1;
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");

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

		vector<string>v2(v1);//拷贝构造
		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;

	}

运行:出错

其实这里的原因还是因为memcpy()浅拷贝导致的对同一块空间析构两次导致的错误。所以这里要将拷贝构造函数进行改造,改造如下:

vector( vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(v.capacity());

			for (auto e : v)
			{
				push_back(e);
			}
			
		}

然后我们的程序便可以跑起来了。

结果:

 

6.其它的构造函数

1.迭代器区间构造函数。

作用:通过某个类型的容器的迭代器来初始化。代码如下:

template<class InputIterator>//在类里面定义一个模板,这样就可以让这个迭代器区间构造的函数更加方便使用
vector(InputIterator first, InputIterator end)
		{
			while (first != end)
			{
				push_back(*first);
				first++;
			}
		}

现在尝试通过下面的代码来进行测试:

void test_vector8()
	{
		vector<string>v1;
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");
		v1.push_back("11111");

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

		vector<string>v2(v1.begin(), v1.end());

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

	}

结果:正常

 2.指定大小和初始化值的函数

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

		}

这里需要注意一点,n的类型得是int才行。要不然在调用这个函数来构造一个vector<int>类型的对象时就会发生类型匹配上的错误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/873262.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入篇【Linux】学习必备:进程理解(从底层探究进程概念/进程创建/进程状态/进程优先级)

深入篇【Linux】学习必备&#xff1a;进程理解(从底层探究进程概念/进程创建/进程状态/进程优先级&#xff09; 一.进程概念(PCB/task_struct)二.查看进程(top/ps)三.创建进程(fork)四.进程状态(僵尸进程/孤儿进程)五.进程优先级(PRI/NI) 一.进程概念(PCB/task_struct) 1.什么…

不同路径——力扣62

文章目录 题目描述解法一 动态规划题目描述 解法一 动态规划 int uniquePaths(int m, int n) {vector<vector

【Java】项目管理工具Maven的安装与使用

文章目录 1. Maven概述2. Maven的下载与安装2.1 下载2.2 安装 3. Maven仓库配置3.1 修改本地仓库配置3.2 修改远程仓库配置3.3 修改后的settings.xml 4. 使用Maven创建项目4.1 手工创建Java项目4.2 原型创建Java项目4.3 原型创建Web项目 5. Tomcat启动Web项目5.1 使用Tomcat插件…

LeetCode150道面试经典题-- 两数之和(简单)

1.题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意…

pytest框架快速进阶篇-pytest前置和pytest后置,skipif跳过用例

一、Pytest的前置和后置方法 1.Pytest可以集成unittest实现前置和后置 importunittestimportpytestclassTestCase(unittest.TestCase):defsetUp(self)->None:print(unittest每个用例前置)deftearDown(self)->None:print(unittest每个用例后置)classmethoddefsetUpClass…

JDK17下载与安装(完整图文教程含安装包)

1.下载JDK17安装包 官网下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads/ 同时提供一份网盘下载地址&#xff0c;大家按需自取&#xff1a;点击下载 JDK 所有版本的安装方法都一样&#xff0c;其他版本也不用重复找教程了。 网盘直接放了 JDK 6 – …

Python教程(8)——一文弄懂Python字符串操作(下)

Python字符串操作 字符串常用方法字符串更多方法介绍 字符串常用方法 字符串在编程中是一种不可或缺的数据类型&#xff0c;它在文本和字符数据时提供了丰富而强大的功能。掌握了字符串的使用方法&#xff0c;你能够更加便捷地进行文本处理、数据操作、用户交互等任务&#xf…

存储器分配算法

1.设计目的与要求 1.1设计目的 本设计的目的是使学生了解动态分区分配方式中使用的数据结构和分配算法&#xff0c;并进一步加深对动态分区存储管理方式及其实现过程的理解。 1.2设计要求 用C语言分别实现采用首次适应算法和最佳适应算法的动态分区分配过程malloc()和回收过程…

多表联合查询

1.创建student表 mysql> CREATE TABLE student ( -> id INT(10) NOT NULL UNIQUE PRIMARY KEY , -> name VARCHAR(20) NOT NULL , -> sex VARCHAR(4) , -> birth YEAR, -> department VARCHAR(20) , -> address VARCH…

【AWS 大赛】亚马逊云科技:2023 直冲云霄训练营入营考试报名与答题答案参考

目录 一、报名 &#xff08;1&#xff09;选择 “解决方案架构师-助理级” &#xff08;2&#xff09;未登录先注册账号 &#xff08;3&#xff09;登录 &#xff08;4&#xff09;报名 &#xff08;5&#xff09;报名成功 二、答题 &#xff08;1&#xff09;开始…

FreeRTOS(互斥信号量)

资料来源于硬件家园&#xff1a;资料汇总 - FreeRTOS实时操作系统课程(多任务管理) 目录 一、互斥信号量的定义与应用 1、互斥信号量的定义 2、互斥信号量的应用 3、简要了解递归互斥信号量 二、优先级翻转问题 1、运行条件 2、优先级翻转编程测试 三、互斥信号量的运…

[HDLBits] Exams/m2014 q3

Consider the function f shown in the Karnaugh map below. Implement this function. d is dont-care, which means you may choose to output whatever value is convenient. //empty

[HDLBits] Exams/2012 q1g

Consider the function f shown in the Karnaugh map below. Implement this function. (The original exam question asked for simplified SOP and POS forms of the function.) //

文本三剑客之grep命令和awk命令 1.0 版本

grep awk 1.grep命令1.1 基本格式1.2 常用选项 2.awk命令2.1 awk工作原理2.2 awk命令格式2.3 awk常用内置变量 1.grep命令 1.1 基本格式 grep [选项]… 查找条件 目标文件1.2 常用选项 选项功能 -m [ x ]匹配x次 后停止,x为具体数字-v取反 -i忽略字符大小写 -n显示匹配的 …

GrapeCity Documents for Excel, Java Edition Crack

GrapeCity Documents for Excel, Java Edition Crack 增加了对SpreadJS.sjs文件格式的支持&#xff1a; 更快地将大型Microsoft Excel文件转换为.sjs格式。 使用较小的占用空间保存导出的文件。 将Excel/SpreadJS功能导入SpreadJS/从SpreadJS导出。 从.sjs文件中压缩的JSON文件…

小程序发布注意事项

1、使用HBuildx的 发布 功能发布小程序&#xff0c;因为编译完的代码目录不是同一个 如果使用 运行 到小程序&#xff0c;最后发布的版本会显示”无法连接本地服务器“ 2、使用unicloud的云服务 uniCloud发行 | uni-app官网 阿里云的unicloud的话&#xff0c;使用request域名…

Docker启动、停止、删除容器的相关指令

关闭容器指令&#xff1a; docker stop name启动命令&#xff1a; docker start name删除容器&#xff1a; docker rm name 或 id查看所有容器id&#xff1a; docker ps -aq删除所有容器&#xff1a; docker rm docker ps -aq开启着的容器是不能被删除的。 查看容器信息&…

华为OD机试 - 最长连续子序列 - 双指针(Java 2023 B卷 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述备注 四、双指针1、双指针是什么&#xff1f;2、Java双指针算法适合解决哪些问题&#xff1f; 五、解题思路六、Java算法源码七、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专…

GUI、多线程编程、网络编程简介

GUI、多线程编程、网络编程简介 文章目录 GUI简介什么是GUIGUI有什么用使用方法 多线程编程什么是多线程编程多线程编程有什么用提高程序的响应能力提高程序的性能实现异步编程并发数据访问和共享资源实现复杂的算法和任务分解 进行多线程编程的步骤 网络编程什么是网络编程网络…

day6 STM32时钟与定时器

STM32时钟系统的概述 概念 时钟系统是由振荡器&#xff08;信号源&#xff09;、定时唤醒器、分频器等组成的电路。 常用的信号有晶体振荡器和RC振荡器。 意义 时钟是嵌入式系统的脉搏&#xff0c;处理器内核在时钟驱动下完成指令执行&#xff0c;状态变换等动作&#xff…