当前位置: 首页 > news >正文

vector的模拟实现01

文章目录

  • vector的模拟实现
    • 构造函数
    • 析构函数
    • 迭代器
    • 容量
      • size
      • capacity
      • reverse
    • 遍历
      • 下标+[]
    • 修改
      • push_back
      • pop_back
      • insert
    • 结语

我们大家有又见面了,给生活加点</font color =red>impetus!!开启今天的编程之路
在这里插入图片描述
今天我们来学习vector。了解一些基本的接口已经迭代器失效问题!!
作者:٩( ‘ω’ )و260
我的专栏:C++初阶,数据结构初阶,题海探骊,c语言
感谢点赞,关注!!

vector的模拟实现

在 C++ 里,std::vector 属于标准模板库(STL)中的容器,它可看作是动态数组。话多多说,直接进入模拟实现。
首先我们先要明白,vectoe底层是数组,如果我们数组中存储的是字符类型,那还要string类干嘛:

1:string后面默认有\0,vector没有
2:接口有较大差异,比如vector中其实没有find函数,只能使用算法库中的函数

其次就是需要定义vector的结构,也许你会认为可以使用顺序表定义的结构,但是在这里,我们为了更加接触底层,在vector的源代码中,他的成员函数是这样定义的:总共有三个,分别为迭代器,名称为:_start,_finish,_end_of_storage。分别代表的是有效位置的第一个元素,有效位置的最后一个元素,有效空间的最后一个位置
我们来看下面的定义:

class vector{
pubilc:
private:
	iterator _start;
	iterator _finish;
	iterator _end_of_storage;
};

其实这样还是会有一点小瑕疵,编译器怎么知道iterator是什么东西吗?
而且,vector的底层是数组,那么数组应该是可以存储各式各样的数据类型,包括整形,浮点数,自定义类型等等,我们再来加以修改一下:

template<class T>
class vector{
pubilc:
	typedef T* iterator;//定义普通迭代器
	typedef const T* const_iterator;//定义const迭代器
private:
	iterator _start;
	iterator _finish;
	iterator _end_of_storage;
}

作用:指向各种数据的迭代器。

因为vector的底层是数组,所以这里的迭代器就可以理解为数组的指针,本质上是因为数组空间是连续的。在这里,我们需要重点学习迭代器失效!!
这里的迭代器的typedef一定要定义在public中,这样外面才能使用。
随后我想补充自己总结的知识点:
1:模版可以做声明和定义分离,但是不要分离到两个文件,否则会发生链接错误
2:利用模版传参一般是传引用,因为是数组,值传递的时候调用拷贝构造,目的是防止深拷贝
3:注意,只有先声明模版,函数才能够传递模版参数,例如

//template<class T>
//void Print(const Mrzeng::vector<T>&res){}

构造函数

在这里我们主要实现是三种构造函数,第一种是无参构造,第二种是有参构造,第三种是利用其他类来进行构造
先来看第一种
因为是无参的构造函数,所以我们构造函数的时候必须要去初始化列表中初始化,但是由于这些都是迭代器可以理解为指针,所以我们直接将三者的初始化为空指针:

vector
:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
{}

接下来我们来完成有参的初始化,但是这里的结果是数组中的数据全部都是相同的,因为这种构造就是用n个val值来初始化vector实例化的对象
直接来看代码:

vector(size_t n,const T& val=T())//匿名对象
{
	assert(n>0);
	reverse(n);
	for(size_t i=0;i<n;i++)
	{	
		push_back(val);
	}
}

虽然说我们还没有实现迭代器以后push_back,但是这个大致思路确实是如此。
首先第一句代码使用了匿名对象:因为外部传给vector的值并不确定,所以vector中有可能能够存储各式各样的值,我们这里只能够使用匿名对象。
匿名对象首先会调用的构造函数,去构造一个自定义类型之后就会传递给vector,那么,有的同学会问,那内置类型怎么办呢?难道内置类型使用匿名对象时会升级为自定义类型
答案确实是如此,接下来利用代码验证:
在这里插入图片描述
接下来实现第三种,使用另一个模版来构造vector,鉴于我们对于后面还没学习,这里主要讲解用法,来看代码:

vector(initializer_list<T> il)
{
	reverse(il.size());
	for(auto& e:il)
	{
		push_back(e);
	}
}

initializer_list是另一类,在这里初始化的时候可以使用任何数据来进行初始化,比如:

vector<int> v1{1,2,3,4,5};

在这里的原理是什么呢?

利用initializer_list初始化时自动开空间,自动生成首尾迭代器,这也是为什么我我没有显示写begin(),end(),但是也能够使用范围for。注意范围for的本质就是迭代器

析构函数

析构函数简单,只需要释放开辟的空间即可,来看下列代码:

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

这里只需要判断是否需要析构即可,因为一开始无参初始化三个成员变量都是空,再来析构的话就显得画蛇添足了!!

迭代器

这里实现开始与结束的迭代器,与string类相差无几,直接来看代码:

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

在迭代器这里有一个细节:

迭代器是左闭右开的类型,类似于[begin(),end()),末尾迭代器并不是包含在内的元素,这里需要记忆

容量

size

size其实就是有效位置的最后迭代器减上有效位置的起始迭代器即可:

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

可以理解为指针:两个指针相减的值就是其中相邻元素的个数,前提是指向同一块空间

capacity

capacity就是容量的最后位置迭代器减上有效位置起始迭代器:

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

可以理解为指针:两个指针相减的值就是其中相邻元素的个数,前提是指向同一块空间

reverse

需要判断什么时候需要扩容,扩容之后又要做什么,代码是否有错误?而在扩容之中我们又需要干什么?
来看下列代码:

void reverse(size_t n)
{
	if(n>_end_of_storage)
	{
		T*tmp=new T[n];
		memcpy(tmp,_start,size()*sizeof(T));//注意memcpy的单位是字节
		delete[]_start;
		_start=tmp;
		//修改成员变量
		_finish=tmp+size();
		_end_of_capacity=tmp+n;
	}
}

这里在_finish和_end_of_storage修改成员变量的时候主要是为了标记这个迭代器是在哪个vector之中的!!
那么我们上面还有没有可以优化的地方呢?上面的代码有没有问题呢?我们来看一下:

首先我们看一下倒数第二行的size,计算是通过_finish-_start实现的,但是我们发现,在求size之前我们就已经对_start进行了变化,此时_finish指向旧的已经被销毁的空间,_start指向新空间的起始位置,此时代码肯定报错:

在这里插入图片描述
再来看这张图理解,所以如果按照这个代码扩容的话,_finish的结果会一直是0。
在这里插入图片描述
最终代码:

void reverse(size_t n)
{
	if(n>_end_of_storage)
	{
		size_t old_size=size();//存储下来这个偏移量
		T*tmp=new T[n];
		if(_start)//如果被拷贝数据此时不是空的话,我再来进行操作,否则就不需要操作了
		{
			memcpy(tmp,_start,old_size*sizeof(T));
			delete[]_start;
		}
		_start=tmp;
		//修改成员变量
		_finish=tmp+old+size();
		_end_of_capacity=tmp+n;
	}
}

遍历

下标+[]

此时与string相差无几,直接来看代码即可:

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

修改

push_back

尾插,首先我们需要先判断是否需要扩容,随后再来进行尾插,最后再对成员变量进行修改:
来看代码:

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

pop_back

对尾部进行删除,删除之后修改成员变量

void pop_back()
{
	_finish--;
}

直接让有效元素减一个就可以了

insert

我们需要在指定位置进行插入操作,首先需要先检查是否需要扩容,随后再来移动元素,最后再插入数据以及修改成员变量。
来看代码:

void insert(iterator pos,const T&x)
{
	if(_finish==_end_of_storage)
	{
		size_t newcapacity=_end_of_storage==0?4:2*capacity();
		reverse(newcapacity);
	}
	iterator it=_finish-1;
	while(it>=pos)
	{
		*(it+1)=*it;
		it--;
	}
	*pos=x;
	_finish++;
}

这样的代码看起来是ok的,但是如果我们去检验,我们会发现一个问题,此时我们会发现我们一直无法退出循环,那这个是什么原因呢?迭代器失效–原因是野指针

我们如果发生了扩容,就会导致旧空间销毁,但是最开始pos迭代器肯定是指向旧空间的,开辟新空间之后pos迭代器变成就是野指针了,所以我们应该及时对pos迭代器进行修改

所以我们这里应该改为:

	if(_finish==_end_of_storage)
	{
		int len=pos-_start;//记录下偏移量
		size_t newcapacity=_end_of_storage==0?4:2*capacity();
		reverse(newcapacity);
		//利用偏移量找到新的pos迭代器
		pos=_start+len;//前面加一个_start主要是为了标记新的数组的偏移量
	}

来看下面图解:
在这里插入图片描述
这里还有一个细节:

如果我们在外面设置了pos指针,插入数据之后一定不要再去使用这个迭代器,因为我不知道是否会产生扩容,不知道这个迭代器是否会被更新,因为我这里是使用的传值调用,里面的操作不会影响外面,里面这个迭代器经历了什么我是一点都不知道的。
那你可能会说,那我们转引用不就好了吗?但是如果是一个迭代器再来进行算术加减法,就会产生一个临时对象记录这个结果,传过去临时对象,不加const的话又是一个权限放大问题,所以我们必须加上const,但是加上const我的pos位置的数据就不能再修改了!!
所以:为了方便,不要在使用调用insert实参为迭代器的变量了!!

示例代码:

	int x = 2;
	cin >> x;
	auto pos = find(v.begin(), v.end(), x);
	if (p != v.end())
	{
		v.insert(pos, x * 10);
		// insert以后pos不能使用,pos可能失效,不要访问这个迭代器
		// cout << *pos << endl;
	}

find是算法库中的函数,如果找到对应位置数值为x的位置,就会放回这个位置的迭代器,,否则会返回last位置的迭代器,代表没有找到。
在这里插入图片描述

结语

感谢大家阅读我的博客,不足之处欢迎与我交流讨论。
时人不识凌云木,直待凌云始道高!!加油!!
在这里插入图片描述

http://www.dtcms.com/a/98886.html

相关文章:

  • C++运算符重载、类的转换构造函数和类型转换函数的基础练习
  • 【SPP】蓝牙串口协议应用层深度解析:从连接建立到实战开发
  • 解决Dubbo3调用Springcloud接口报No provider available from registry RegistryDirectory
  • 【java基础】Java 泛型
  • IPv6 Over IPv4 自动 6to4 隧道
  • Altium Designer——同时更改多个元素的属性(名称、网络标签、字符串标识)
  • OpenBMC:BmcWeb 生效路由5 优化trie
  • Unity高渲染管线
  • 经济均衡问题建模与求解:单一市场供需平衡分析
  • 蓝桥杯单片机刷题——E2PROM记录开机次数
  • R CSV 文件处理指南
  • 项目如何安装本地tgz包并配置局部registry
  • NixVis 开源轻量级 Nginx 日志分析工具
  • 鸿蒙开发:了解Canvas绘制
  • [C++项目]高并发内存池性能测试
  • 音乐推荐系统的研究与应用
  • JAVA学习*单列模式
  • Linux进程状态补充(10)
  • 18-动规-子序列中的 k 种字母(中等)
  • Python --- .flush() 强制输出缓冲区的数据(缓冲区未满)
  • 代码随想录算法训练营--打卡day3
  • HTML元素小卖部:表单元素 vs 表格元素选购指南
  • 从零开始研发GPS接收机连载——19、自制GPS接收机的春运之旅
  • 《Spring Cloud Eureka 高可用集群实战:从零构建高可靠性的微服务注册中心》
  • 【RabbitMQ】Linux上安装RabbitMQ详细步骤
  • 全球化2.0 | ZStack举办香港Partner Day,推动AIOS智塔+DeepSeek海外实践
  • 嵌入式libc
  • [创业之路-344]:战略的本质是选择、聚焦, 是成本/效率/低毛利优先,还是差易化/效益/高毛利优先?无论是成本优先,还是差易化战略,产品聚焦是前提。
  • 基于HTML5和CSS3实现3D旋转相册效果
  • linux课程学习二——缓存