手撕vector

文章目录

    • 一.vector的基本结构
    • 二.构造函数调用不明确
    • 三.迭代器失效(其实是野指针问题)
      • a.扩容导致的迭代器失效
      • b.意义不同
    • 四.深层次的深浅拷贝
    • 五.整体代码实现

有了前面模拟实现string的经验,vector对大家来说也不会算很难。vector本质也就是一个空间可以动态变化的数组,所以这里就挑一些些容易踩坑的地方讲解一下,在最后会附上我的完整代码。

因为vector中没有使用size以及capacity而是使用了迭代器,在避免了整形转无符号整形的同时,也产生了如迭代器失效的新问题。此外因为vector本身是一个模板,也就是vector可以再嵌套一个vector,如果在扩容阶段会有一个深层次的深拷贝问题。


一.vector的基本结构

class vector {
public:
  typedef T value_type;//可以看到这里所谓的value_type其实是T的重命名
    
  typedef value_type* iterator;//迭代器
  typedef const value_type* const_iterator;//const迭代器
protected:
  iterator start;//这个是指向数组空间的首地址,相当于顺序表中的int*a
  iterator finish;//这个相当于size
  iterator end_of_storage;//相当于capacity
}

以上是我从sgi版本源代码中抽取出来的主要框架,在我们现阶段的学习中不建议去大量的阅读源代码,但可以从某些实现细节上以及它的整体框架去阅读。

这里附上sgi版本的stl30源码:

链接:https://pan.baidu.com/s/1lKls7ne0iiZJz6tsQEgufw
提取码:0325

下面再通过一张图来看一下整体结构

在这里插入图片描述

通过上图不难发现,我们可以通过end _ of _storag - start 得到vector的容量,通过finish - start得到vector的size

虽然我的模拟实现整体是跟着sgi版本的,但是在扩容的时候选择了扩二倍。

学习STL并不是为了造轮子,我们也很难造出比大佬们更好的轮子,之所以模拟实现STL是为了能更好的弄清STL中的一些小细节。

二.构造函数调用不明确

	//使用n个val来构造
		vector(size_t n, const T& val = T())//这里的size_t会导致调用不明确
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n);//提前开好空间,减少后续扩容带来的损耗
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);//复用尾插
			}
		}

		//用一段迭代器区间来构造this
		template <class InputIterator>
		vector(InputIterator first,InputIterator end)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			while (first != end)
			{
				push_back(*first);
				first++;
			}
		}

在这里插入图片描述

这里说什么非法的间接寻址,这个报错属实恶心,原因如下:

在模板中提到过,编译器会自主选择最匹配的函数,在这个构造函数,我们本想调用一个构造函数(使用n个val构造),但是我们对n使用了size_t类型,而3和6都是int类型。

对于第一个构造函数来说,编译器需要将int类型强转为size_t,但是第二个构造函数是一个模板,可以直接将类型推演成int,直接就将两个类型都匹配上了不用强转。那么编译器选择最合适的当然会选择第二个模板参数的构造函数咯,但是第二个构造函数内部对first进行了解引用,这里传过去的first只是一个整形,所以就报错了。

解决办法就是多重载一个int类型的

三.迭代器失效(其实是野指针问题)

a.扩容导致的迭代器失效

void insert(iterator pos, const T& val)//这里给个返回值是为了解决迭代器失效,pos异地扩容的情况下也是会失效,所以内部处理需要更新pos位置
		{
			//如果size为零呢?对于这个问题,我们的迭代器就控制了第一次插入是不能用insert的,也就是说如果使用迭代器作为参数,则vector必须要有元素
			if (size() == 0)
			{
				push_back(val);
			}//其实不判断也行
			else
			{
				assert(pos >= _start);
				assert(pos < _finish);//分开写能更好的界定错误



				if (_finish == _endofstorage)
				{
					
					int newCapacity = capacity() == 0 ? 4 : 2 * capacity();
					reserve(newCapacity);
					
				}


				iterator end = _finish;
				while (end > pos)
				{
					*end = *(end - 1);
					end--;
				}
				//减完以后直接插入
				*pos = val;

				++_finish;

			}
			
		}

在这里插入图片描述

第五个值变成了随机值,是因为扩容可能是异地扩,一旦异地扩那么pos原本指向的那块空间就被释放了,pos也就变成了一个野指针因此无法在正确的位置插入我们想要的元素。

在这里插入图片描述

解决办法就是在如果要扩容(扩容有异地和原地,这里一律认为是异地扩),那么就更新pos的位置。

在扩容之前记住_start 和pos的相对位置,然后扩容之后再给 _start加上这个相对长度就可以在新空间中拿到正确的pos位置。

此外还要给一个返回值,因为我们将it传过去以后,it变成了野指针不能再使用了,如果我还要使用就也要更新,使用返回值来更新是一个好办法,更改代码如下:

iterator insert(iterator pos, const T& val)//这里给个返回值是为了解决迭代器失效,pos异地扩容的情况下也是会失效,所以内部处理需要更新pos位置
		{
			//如果size为零呢?对于这个问题,我们的迭代器就控制了第一次插入是不能用insert的,也就是说如果使用迭代器作为参数,则vector必须要有元素
			if (size() == 0)
			{
				push_back(val);
			}
			else
			{
				assert(pos >= _start);
				assert(pos < _finish);//分开写能更好的界定错误



				if (_finish == _endofstorage)
				{
					int len = pos - _start;
					int newCapacity = capacity() == 0 ? 4 : 2 * capacity();
					reserve(newCapacity);
					//扩完容就要插入,防止异地扩解决失效,首先要更新pos

					pos = _start + len;
				}


				iterator end = _finish;
				while (end > pos)
				{
					*end = *(end - 1);
					end--;
				}
				//减完以后直接插入
				*pos = val;

				++_finish;

			}
			return pos;
		}

b.意义不同

除了在插入的时候有迭代器失效的问题以外,在删除元素的时候也会有迭代器失效的问题,相对于插入而言删除时的失效稍微难理解一些。下面我们来看这样一个现象:

void Test_vector3()
{
    
	std::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

std:vector<int>::iterator it = find(v.begin(), v.end(), 3);
	v.erase(it);
	cout << *it << endl;
	
}

int main()
{
    Test_vector3();
    return 0;
}

调用库中的vector进行删除操作,删完以后再访问一下这个迭代器,看起来没什么问题,但是编译器报错了哦。
在这里插入图片描述

连读操作都报错了,那么写操作更不用说肯定是会报错的,这里报错是因为VS认为删除操作以后迭代器是失效的了的,它调用了一个函数去检查。如果将同样的代码放到g++中去测试,就会发现并不会报错。


如果去考虑最边界的情况,比如it迭代器是finish的前一个,这是删除以后这个迭代器确确实实是会失效的。

在这里插入图片描述

所以不论是为了程序的稳定性还是可移植性,都建议将erase以后的迭代器认为是失效的。

处理办法和insert类似,加一个返回值即可,库中也是给定了返回值的。

	iterator erase(iterator pos)//这里有一个迭代器失效,并不是野指针类的,而是因为意义不同了,有时候也是真的会野指针
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator end = pos;
			while (end < _finish)
			{
				*end = *(end + 1);
				end++;
			}
			_finish--;

			return pos;
		}

四.深层次的深浅拷贝

引发这个问题的主要原因是因为我在实现扩容的时候采用的拷贝方式是使用memcpy函数进行拷贝的:

	void reserve(size_t n)
		{
			//不缩容
			if (n > capacity())
			{
				//新开辟一块空间,释放旧空间
				T* tmp = new T[n];
				int oldsize = size();
				if (_start)
				{
					//之前的空间不是空,就拷贝
					memcpy(tmp, _start, oldsize * sizeof(T));//这里有一个浅拷贝问题,memcpy是浅拷贝,不得行

				}

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

		}

众所周知memcpy的拷贝方式是按字节拷贝的,也就是浅拷贝,但是vector内部是一个模板,也就是说可能存在这样的情况:vector<vector<int>>

如果vector的模板参数还是一个自定义类型的话,就很容易造成深层次的浅拷贝问题。

在这里插入图片描述

浅拷贝图解

在这里插入图片描述

最核心的地方在于,memcpy只是按字节拷贝,没有改变_start的指向。而delete又会调用析构函数,释放了 _start的空间,使其变成了野指针。

解决

void reserve(size_t n)
		{
			//不缩容
			if (n > capacity())
			{
				//新开辟一块空间,释放旧空间
				T* tmp = new T[n];
				int oldsize = size();
				if (_start)
				{
                    for (size_t i = 0; i < oldsize; i++)
                    {
                        tmp[i] = _start[i];
                    }

				//要开空间然后拷贝再更新指针指向
					delete[]_start;
				}

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

		}

这里复用了赋值重载,赋值重载使用的是传值传参,形参是实参的一份临时拷贝,所以在传参的时候就已经开辟好了空间,并且在内部调用了swap函数,交换了_start的指向。

五.整体代码实现

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

//直接上手手搓一个

namespace wbm
{
	template<class T>

	class vector
	{
	public:
		//vector采用迭代器,这里使用指针实现
		typedef T* iterator;
		typedef const T* const_iterator;

		//首先上来手搓构造
		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{}

		//使用n个val来构造
		vector(size_t n, const T& val = T())//这里采用的是匿名对象初始化T
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n);//提前开好空间,减少后续扩容带来的损耗
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);//复用尾插
			}
		}

		//用一段迭代器区间来构造this
		template <class InputIterator>
		vector(InputIterator first,InputIterator end)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			while (first != end)
			{
				push_back(*first);
				first++;
			}
		}

		//拷贝构造可以复用上面利用迭代器的构造
		vector(vector& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			vector<T> tmp(v.begin(), v.end());//创建一个临时对象,然后将临时对象用v的迭代器区间初始化
			swap(tmp);
		}


		//析构
		~vector()
		{
			//释放空间然后置空
			delete[]_start;
			_start = _finish = _endofstorage = nullptr;
		}

		//快速搓起来,先整迭代器和尾插, vector不提供头插函数因为效率太低,如果偶尔头插可以使用insert

		//要插入元素就会涉及扩容的问题,所以要先实现扩容,要扩容就要判断容量,所以要给获取容量等函数

		void reserve(size_t n)
		{
			//不缩容
			if (n > capacity())
			{
				//新开辟一块空间,释放旧空间
				T* tmp = new T[n];
				int oldsize = size();
				if (_start)
				{
					//之前的空间不是空,就拷贝
					//memcpy(tmp, _start, oldsize * sizeof(T));//这里有一个浅拷贝问题,memcpy是浅拷贝,不得行
				for (size_t i = 0; i < oldsize; i++)
				{
					tmp[i] = _start[i];
				}

				//要开空间然后拷贝再更新指针指向
					delete[]_start;
				}

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


		}

		void resize(size_t n,T val=T())//要有默认初始值,初始化后面的值,这里是生成一个匿名对象来初始化它
		{
			//在我看来改变size不一定要改变capacity,如果要更改的size大于capacity,那就必须扩容
			if (n > capacity())
			{
				int newCapacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(newCapacity);
			}
			//改变size就是改变_finish
			if (n > size())
			{
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
			else
			{
				_finish = _start + n;
			}
		}


		void push_back(const T& x)
		{
			//_finish指向的是末尾元素的下一个位置
			//首先判断一下是否需要扩容那个
			if (_finish == _endofstorage)
			{
				int newCapacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(newCapacity);
			}

			*_finish = x;
			_finish++;

		}

		void pop_back()
		{
			assert(!empty());//是空还删毛
			//尾删,直接挪动_finish
			_finish--;
		
		}

		void clear()//不动空间
		{
			_finish = _start;
		}

		//iterator insert (iterator position, const value_type& val);

		iterator insert(iterator pos, const T& val)//这里给个返回值是为了解决迭代器失效,pos异地扩容的情况下也是会失效,所以内部处理需要更新pos位置
		{
			//如果size为零呢?对于这个问题,我们的迭代器就控制了第一次插入是不能用insert的,也就是说如果使用迭代器作为参数,则vector必须要有元素
			if (size() == 0)
			{
				push_back(val);
			}
			else
			{
				assert(pos >= _start);
				assert(pos < _finish);//分开写能更好的界定错误



				if (_finish == _endofstorage)
				{
					int len = pos - _start;
					int newCapacity = capacity() == 0 ? 4 : 2 * capacity();
					reserve(newCapacity);
					//扩完容就要插入,防止异地扩解决失效,首先要更新pos

					pos = _start + len;
				}


				iterator end = _finish;
				while (end > pos)
				{
					*end = *(end - 1);
					end--;
				}
				//减完以后直接插入
				*pos = val;

				++_finish;

			}
			return pos;
		}

		//iterator erase (iterator position);//删除一个元素
		//iterator erase(iterator first, iterator last);//删除一个区间内的元素

		iterator erase(iterator pos)//这里有一个迭代器失效,并不是野指针类的,而是因为意义不同了,有时候也是真的会野指针
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator end = pos;
			while (end < _finish)
			{
				*end = *(end + 1);
				end++;
			}
			_finish--;

			return pos;
		}

		//赋值重载,自己给自己赋值的情况太少,这里不做处理,但是库中有处理
		vector<T>& operator=(vector<T>& x)
		{
			//赋值重载双方本来都有空间,这里用现代写法

			swap(x);//默认this占据第一个参数
			return *this;
		}

		//重载方括号
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

		void swap(vector<T>&x)
		{
			std::swap(_start, x._start);
			std::swap(_finish, x._finish);
			std::swap(_endofstorage, x._endofstorage);
		}

		iterator begin()const
		{
			return _start;
		}
		iterator end()const
		{
			return _finish;
		}

		//尾插实现之前首先要提供容量等接口函数

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

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

	private:
		iterator _start;//这个代表可更改大小的数组
		iterator _finish;//这个类似于size
		iterator _endofstorage;//capacity;

	};
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ORACLE】极速通关Oracle23c开发者免费版连接

前言 oracle23c开发者免费版已经于2023年4月4日(北京时间)推出&#xff0c;并且官方也公布了安装介质的下载地址&#xff0c;有RPM安装包、VM虚拟机、docker镜像&#xff08;下载链接见文末&#xff09;。 由于最近工作比较忙&#xff0c;暂时无法写一篇内容丰富的测试&#x…

springboot树形结构接口, 懒加载实现

数据库关系有父子id的, 作为菜单栏展示时需要用前端需要用到懒加载, 所谓懒加载就是接口有一个标志位isLeaf, 前端请求后通过该字段判断该节点是否还有子节点数据 创建数据库表 t_company_info结构有id和parentId标识, 用来表示父子关系 /*Navicat Premium Data TransferSourc…

大数据环境-云平台(阿里云)

由于电脑配置原因&#xff0c;无法在本地利用虚拟机搭建环境&#xff0c;因此使用云平台来当做学习的环境。 本节内容参考&#xff1a; 【2023新版黑马程序员大数据入门到实战教程&#xff0c;大数据开发必会的Hadoop、Hive&#xff0c;云平台实战项目全套一网打尽-哔哩哔哩】 …

72-Linux_线程同步

线程同步一.什么是线程同步二.线程同步的方法1.互斥锁(1)什么是互斥锁(2)互斥锁的接口(3)互斥锁的使用(例题)2.信号量(1)什么是信号量(2)信号量的接口(3)信号量的使用(例题)a.循环打印ABCb.:主线程获取用户输入,函数线程将用户输入的数据存储到文件中;3.读写锁(1)读写锁的接口(…

R语言|plot和par函数绘图详解,绘图区域设置 颜色设置 绘图后修改及图像输出

plot()函数 plot()函数是R中最基本的绘图函数&#xff0c;其实最简单、最基础的函数&#xff0c;这也就意味着其具有更多的可操作性。 plot(x,y,...) 在plot函数中&#xff0c;只需指定最基本的x和y轴对应数据即可进行图像的绘制&#xff0c;x和y轴数据分别为两个向量或者是只…

年薪40W测试工程师成长之路,你在哪个阶段?

对任何职业而言&#xff0c;薪资始终都会是众多追求的重要部分。前几年的软件测试行业还是一个风口&#xff0c;随着不断地转行人员以及毕业的大学生疯狂地涌入软件测试行业&#xff0c;目前软件测试行业“缺口”已经基本饱和。当然&#xff0c;我说的是最基础的功能测试的岗位…

WIN10無法再使用 IE 瀏覽器打开网页解决办法

修改 Registry&#xff08;只適用 Win10&#xff09; 微軟已於 2023 年 2 月 14 日永久停用 Internet Explorer&#xff0c;會透過 Edge 的更新讓使用者開啟 IE 時自動導向 Edge&#xff0c;其餘如工作列上的圖示&#xff0c;使用的方法則是透過「品質更新」的 B 更新來達成&am…

NoSQL与Redis五次作业回顾

文章目录1. 作业12. 作业23. 作业34. 作业45. 作业51. 作业1 要求&#xff1a; 在 VM 上安装 CentOS Linux 系统&#xff0c;并在 Linux 上通过 yum 安装 C编译器&#xff0c;对 Redis 进行编译安装。 观察 Redis 目录结构&#xff0c;使用 redis-server 启动服务器&#xff…

Android---Jetpack之DataBinding

DataBinding 的意义 让布局文件承担了部分原本属于页面的工作&#xff0c;使页面与布局耦合度进一步降低。 DataBinding 的应用 使用 dataBinding 需要在 gradle 里添加如下代码 dataBinding{enabled true} 应用实现 activity_main.xml <?xml version"1.0" e…

python argparse的使用,本文基本够用

一、前言 在学习深度学习会发现都比较爱用python这个argparse&#xff0c;虽然基本能理解&#xff0c;但没有仔细自己动手去写&#xff0c;因此这里写下来作为自己本人的学习笔记 argparse是python的一个命令行参数解析包&#xff0c;在代码需要频繁修改参数时&#xff0c;方便…

Shell编程之免交互

目录交互的概念与Linux中的运用Here Document 免交互tee命令重定向输出加标准输出支持变量替换多行注释Expect实例操作免交互预设值修改用户密码创建用户并设置密码实现 ssh 自动登录交互的概念与Linux中的运用 交互&#xff1a;当计算机播放某多媒体程序的时候&#xff0c;编…

步进频雷达的一维距离像matlab仿真

步进频雷达的一维距离像matlab仿真发射与回波信号模型距离高分辨原理仿真分析不进行步进频高分辨一维距离像进行步进频高分辨一维距离像代码发射与回波信号模型 步进频率信号发射得的是一串窄带的相参脉冲&#xff0c;每个脉冲的载频之间是均匀线性步进的&#xff0c;经过相参本…

Spring依赖注入详解

1.set注入 启动容器后看看到底能不能拿到teacherService的值。可以看到拿到了值。我们具体来分析怎么注入的 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean 发现pvs里面有一个我们自己set的值 直接进行属性赋值。 org.springf…

【创作赢红包】Python第3章 流程控制

这里写目录标题【本章导读】真值测试比较运算成员运算for循环while循环项目实训1项目实训2项目实训3项目实训4&#xff1a;项目实训5&#xff1a;项目实训6&#xff1a;项目实训7&#xff1a;项目实训8项目实训9&#xff1a;项目实训10:项目实训11&#xff1a;项目实训12&#…

【Redis7】Redis7 十大数据类型

【大家好&#xff0c;我是爱干饭的猿&#xff0c;本文重点介绍Redis7 十大数据类型。 后续会继续分享Redis7和其他重要知识点总结&#xff0c;如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下吧】 上一篇文章&#xff1a;《【Redis7】Redis7概述、安装…

PCB生产工艺流程五:PCB生产工艺流程的第3步,钻孔的分类及目的

PCB生产工艺流程五&#xff1a;PCB生产工艺流程的第3步&#xff0c;钻孔的分类及目的 今天第五期的内容就是详细讲述PCB工艺流程第三步——钻孔&#xff0c;忘记第二步的小伙伴看这里 PCB工艺流程第2步&#xff0c;你了解多少&#xff1f; 钻孔的目的 在板面上钻出层与层之间…

【Java项目】SpringBoot实现一个请求同时上传多个文件和类并附上代码实例

文章目录前言文件多线程兼多文件上传RequestParam和RequestPart的区别前言 项目在做二手市场&#xff0c;然后商品的提交我们希望对商品的描述和商品的照片能一起传递到同一个接口来统一处理&#xff0c;而不是分发到两个接口中去处理&#xff0c;因为如果分到两个接口那么会特…

大语言模型带来的一些启发

仅代表个人看法&#xff0c;不喜勿喷。 The limits of my language means the limits of my world. (Ludwig Wittgenstein) 我的语言的极限意味着我的世界的极限。——维特根斯坦 大语言模型解决的不仅是处理文本相关问题&#xff0c;它带来的是人对世界的理解&#xff0c;或者…

华为网络篇 单臂路由-17

实验难度 2实验复杂度2目录 一、实验原理 二、实验拓扑 三、实验步骤 四、实验过程 总结 一、实验原理 单臂路由&#xff08;router-on-a-stick&#xff09;是指在路由器的一个接口上通过配置子接口&#xff08;或“逻辑接口”&#xff0c;并不存在真正物理接口&…

EasyExcel的简单使用(easyExcel和poi)

EasyExcel的简单使用 前言 Excel读 1.实体类 2.读监听器与测试类 3.输出结果 Excel写 1.实体类 2.写入Excel的测试类 3.输出结果 填充Excel 1.Excel模板 2.测试类 3.输出结果 前言 EasyExcel类是一套基于Java的开源Excel解析工具类&#xff0c;相较于传统的框架如Apache poi、…