C++《vector的模拟实现》

在之前《vector》章节当中我们学习了STL当中的vector基本的使用方法,了解了vector当中各个函数该如何使用,在学习当中我们发现了vector许多函数的使用是和我们之前学习过的string类的,但同时也发现vector当中一些函数以及接口是和string不同的。那么接下来在本篇我们就将试着模拟实现vector,在模拟实现过程当中我们就会了解当vector当中迭代器失效的问题,相信在本篇之后你会对vector有更深的了解,接下来就开始本篇的学习吧!!!


 1.实现各个函数之前的工作 

在模拟实现我们先要解决在模拟实现vector过程文件该如何创建

在vector由于是要是要使用到类模板来实现,因此在实现就和实现string不同不再将vector成员函数的声明和定义分离到两个文件,因此在此就创建一个头文件vecor.h来存放类vector的实现,在创建一个.cpp文件test.cpp来测试在vector当中实现的各个成员函数是否满足要求以及是否能正常运行。

注:在此为了避免我们实现的vector和std命名空间当中的vector冲突,因此就先新创建一个命名空间,之后将我们模拟实现的vector类放在创建的命名空间内,这样就不会出现冲突了

完成了文件的创建之后接下来就来分析在我们模拟实现的类vector当中成员变量该如何创建

在此你可能就会直接认为由于vector就是顺序表,那么就直接和之前在数据结构当中实现顺序表一样使用三个变量来实现即可,第一个变量就是指向数组的指针;第二个变量就是底层数组的有效元素个数;最后一个变量就是数组的内存空间大小


以上你这种也是可以实现的,但是在通过vector源码就可以发现在实现vector时,vector当中的成员变量是三个迭代器,这时你可能就会疑惑这三个迭代器分别表示的是什么呢?

其实在以上三个迭代器当中start指向的是vector对象内第一个元素,finish指向的是vector对象当中最后一个有效元素之后的位置,end_of_storage指向的是vector对象当中最后内存空间之后的位置。并且在此我们还要了解到由于vector的底层是用数组来实现的那么各个元素在物理逻辑上就是连续存放的,那么在vector当中迭代器底层就可以直接使用对应的指针来实现。那么这时将finish减start就可以得到有效元素,将end_of_storage减finish就可以此时底层数组的内存空间大小

因此通过以上的分析就可以发现使用vector源码当中的成员变量这种形式也是可以满足我们的要求的,那么接下来在模拟实现vector当中也按照源码的这种方式来实现成员变量

#include<iostream>
#include<assert.h>using namespace std;namespace zhz
{template<class T>class vector{public://成员函数. . .private:T* _start = nullptr;T* _finish = nullptr;T* _endofstorage = nullptr;};}

解决了以上的问题接下来就可以开始实现vector当中的各个成员函数了

2.vector模拟实现

在模拟实现vector我们是先实现插入和删除的函数,这时因为在构造函数当中通过调用插入的函数就可以实现各个接口的构造函数,这样就不需要我们自己来实现插入了这就让构造函数的代码写起来简单多了

2.1 size和capacity

在以上我们就分析出了在vector当中该如何来得到size与capacity,因此接下来就直接实现代码

在vector.h内实现函数的代码

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

2.2 下标+[ ]访问

在vector由于各个元素之间的物理空间是连续的,那么就可以实现[ ]的运算符重载函数,在此要实现const与非const对象的两个版本

在vector.h内实现函数的代码

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

注:在此在使用[ ]来访问vector对象内的元素不能超出对象内底层数组的大小,因此在此在实现 [ ]的运算符重载函数就需要使用assert断言

2.3 迭代器

在vector当中迭代器的实现直接使用指针来使用就可以满足要求,在此vector的迭代器就是原生态指针T*, 因此在此在实现begin和end函数之前先要将指针重命名为迭代器

typedef T* iterator;//普通迭代器
typedef const T* const_itreator;//const迭代器

完成以上操作就可以来实现begin和end函数的代码了

在vector.h内实现函数的代码

iterator begin()
{return _start;
}iterator end()
{return _finish;
}
//const迭代器
const_itreator begin()const
{return _start;
}
const_itreator end()const
{return _finish;
}

2.4 reserve和resize

通过之前的学习我们了解过了reverse是用于调整内存空间的大小,resize是用于调整有效元素,那么接下来我们就试着来实现这两个函数

先在vector.h内实现reserve函数的代码

在此和之前string实现一样只有当指定的大小比当前的空间大时才进行调整,这时要将原内存空间调整为大小为n的内存空间就需要先申请大小为n个变量的空间之后再将原空间内的数据拷贝到新申请的内存空间内,再将原空间释放。最后将类成员变量的指向改变就完成了

这时reserve实现出的代码就如下所示:

void reserve(int n)
{if (n > capacity()){T* tmp = new T[n];memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = _start + size();_endofstorage = _start + n;}
}

以上的代码看起来完全可以满足我们的要求,但是其实存在两个严重的问题,你能看出来吗?

 首先是在以上代码中我们将_start指向tmp指向的内存空间后,_finish = _start + size()这时在这条语句其实调用size()函数就有问题了,这时在进入size()函数之后由于_start已经指向新的内存空间但是_finish还是指向原空间,再将这两个指针相减就无法得到有效元素个数,并且会导致程序奔溃

那么要解决以上这个问题就先再改变_start之前创建一个变量oldsize将原来的有效元素个数存储在该变量内,之后_finish = _start + size()修改为_finish = _start + oldsize就可以解决该问题

void reserve(int n)
{if (n > capacity()){T* tmp = new T[n];size_t oldsize = size();memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = _start + oldsize;_endofstorage = _start + n;}
}

以上代码其实还存在一个问题就是memcpy进行的是浅拷贝,这在T是内置类型时确实没什么问题,但我们实现的vector是类模板那么就是为了也能满足vector内的元素是自定义类型这种情况,那么这时再进行浅拷贝就会在使用delete出现问题
就例如当vector对象内的元素是string类型时

在使用完memcpy之后就会使得原内存空间内的string对象内的指针和新空间内string对象对象的指针指向同一块空间,这在调用delete将原空间释放时由于会想调用string的析构函数将string对象内指针指向的空间释放,那么在这之后就会使得在新空间内的string对象内的指针变为空指针 

那么要解决以上这个问题就要再拷贝时进行深拷贝而不是浅拷贝

void reserve(int n)
{if (n > capacity()){T* tmp = new T[n];size_t oldsize = size();//memcpy(tmp, _start, sizeof(T) * size());//memcpy完成的是浅拷贝for (int i = 0; i < oldsize; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = _start + oldsize;_endofstorage = _start + n;}
}

完成了reserve函数之后接下来在vector.h内实现resize函数的代码

在此在resize函数当中要分为两种情况一种是当要调整的大小比原有效元素个数小时就直接改变_finish指针指向即可,但当要调整的大小比原有效元素个数大时就要从_finish指针开始插入新的元素;并且在最后还要改变_finish指针指向

这时resize实现出的代码就如下所示:

void resize(size_t n,T x=T())
{if (n < size()){_finish = _start + n;}else{	reserve(n);//当n比原内存空间还大时,避免多次扩容while (_finish < _start + n){*_finish = x;_finish++;}}
}

注:在以上代码在插入元素后使用缺少参数T(),在此使用到了匿名对象,这时由于类vector是类模板那么变量x的类型就是不确定的,因此就不能直接将变量的缺省值使用0或者nullptr来实现。因此在此就可以使用到之前在类和对象当中学到的匿名对象来实现这个缺省值

2.5 insert和erase

通过之前的学习我们知道insert和erase是分别实现任意位置的插入与删除,并且在vector当中这两个函数的接口都是迭代器

先在vector.h内实现reserve函数的代码

在insert函数当中要实现的是在pos迭代器位置插入指定的数据,在此在插入数据之前要先检查空间是否已经满了,如果满了就要先进行扩容,并且在此在vector当中我们是使用指针来实现遍历的这就不会出现之前我们在string当中实现insert使用下标时的问题

void insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);//判断pos指针是否合法if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : 2 * capacity());//扩容}iterator i = _finish -1;while (pos <= i){*(i + 1) =*i;i--;}*pos = x;_finish++;}

在以上我们就实现了insert的代码,但是以上代码其实存在迭代器失效的问题,首先在解决以上问题之前我们先要来了解什么是迭代器失效

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

在了解了什么是迭代器失效之后接下来就可以来分析为什么以上的代码存在迭代器失效的问题了

在插入前当内存空间不足时也就是_finish = _endofstorage时就需要先进行扩容,那么在调用reserve之后_start就指向新的内存空间,但问题是此时指针pos还指向原来的内存空间,但是原内存空间内的数据已经被释放,此时pos指针就变为空指针,所以之后的将要插入位置之后的数据都往后移动一位时就会造成程序奔溃。

所以要解决以上pos迭代器失效的问题就要在扩容之后更新迭代器,并且将该函数的返回值为新插入位置的迭代器

iterator insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);//判断pos指针是否合法if (_finish == _endofstorage){size_t len = pos - _start;//记录pos迭代器位置reserve(capacity() == 0 ? 4 : 2 * capacity());//扩容pos = _start + len;//更新pos迭代器}iterator i = _finish;while (pos <= i){*(i + 1) =*i;i--;}*pos = x;_finish++;return pos;
}

完成了insert函数之后接下来在vector.h内实现erase函数的代码

在此erase函数就不会出现insert当中pos指针为空的情况,因此你可能就会直接实现出以下代码

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

但其实迭代器失效除了包括扩容引起的野指针,其实还包括删除数据,导致数据挪动,原本的迭代器已经不是指向之前的位置了,这时该迭代器其实野失效了,这是因为之后再使用原来的迭代器可能会导致逻辑问题

就例如以下示例:

#include <iostream>
using namespace std;
#include <vector>int main()
{int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找3所在位置的iteratorvector<int>::iterator pos = find(v.begin(), v.end(), 3);// 删除pos位置的数据,导致pos迭代器失效。v.erase(pos);cout << *pos << endl; // 此处会导致非法访问return 0;
}

erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理
论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end
的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素
时,vs就认为该位置迭代器失效了。

再来看以下代码,以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?

#include <iostream>
using namespace std;
#include <vector>
int main()
{vector<int> v{ 1, 2, 3, 4 };auto it = v.begin();while (it != v.end()){if (*it % 2 == 0)v.erase(it);++it;}return 0;
}
int main()
{vector<int> v{ 1, 2, 3, 4 };auto it = v.begin();while (it != v.end()){if (*it % 2 == 0)it = v.erase(it);else++it;}return 0;
}

以上两段代码再VS下只有第二段代码是能通过的,第一段代码会由于迭代器失效直接报错。而在Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端,所以以上两段代码都能实现要求

那么在VS当中为什么在出现使用失效的迭代器就直接报错呢?
这是由于在一些情况下使用失效的迭代器会造成程序奔溃,就例如将以上数组v内数据改为以下时

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

这时再将以上第一段代码中在将数组中的第一个2删除之后迭代器it就指向了3,这就会使得数组中的第二个2不会被删除,并且在删除最后的2时这时由于进入erase函数之后只会让_finish--,这时删除完2之后继续it++,在此之后it就一直不会等于_finish,所以while就会死循环,造成越界。这种情况下在Linux下也会出现错误

通过以上的分析就知道为什么VS下一旦出现了使用失效的迭代器就报错,那么为了避免使用失效的迭代器,在我们模拟实现的erase也要作出修改

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

在以上代码将pos迭代器之后位置的迭代器作为erase的返回值,这样在调用了erase之后就可以通过其返回值来更新迭代器

2.6 push_back与pop_back

在以上我们实现insert和erase函数之后,在实现尾插push_back和尾删pop_back函数可以直接通过调用insert和erase来实现

void push_back(const T& x)
{insert(end(), x);
}bool Empty()
{return _start == _finish;
}void pop_back()
{assert(!Empty());erase(end()-1);
}

2.7 swap

在vector当中实现swap就可以避免在调用swap时调用到算法库内的swap从而导致深拷贝,而在我们在vector内实现的swap只需要交换指针即可实现vector对象的交换

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

2.7 构造函数

在以上实现了插入函数之后接下来实现vector构造函数的各个接口就很简单了

//空构造
vector()
{}//使用迭代器区间构造
template<class InputIterator>
vector(InputIterator fist, InputIterator last)
{while (fist != last){push_back(*fist);fist++;}
}//调用initializer_list构造
vector(initializer_list<T> t1)
{reserve(t1.size());for (auto& x: t1){push_back(x);}}vector(int n, const T& x = T())
{reserve(n);while (n--){push_back(x);}
}//n个x构造
vector(size_t n, const T& x = T())
{reserve(n);while(n--){push_back(x);} 
}//拷贝构造
vector(const vector<T>& x)
{reserve(x.capacity());for (auto& e : x){push_back(e);}
}

 在以上构造函数中在实现n个变量构造时为什么还要提供以下接口呢?

这是因为当vector对象类型是int时,在使用原本的n个变量构造时由于迭代器区间构造参数类型会比n个变量构造的参数类型更加匹配,这时就构造会调用到迭代器区间构造。因此实现以上函数就是为了避免这个问题

2.8 析构函数

在vector当中析构函数的作用是在对象清除时完成资源清理的工作,又因为在vector当中底层的资源就是数组,那么析构函数内要实现的就是将start指向的内存空间释放

完成了分析接下来就在vector.h内实现函数的代码

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

2.9 赋值运算符重载

在vector赋值运算符重载函数和string中一样可以直接使用以下的现代写法实现

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

以上就是本篇的所有内容了,希望能得到你的点赞、收藏❤️

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Postgresql中对空间数据进行表分区的实践

在数据库管理中&#xff0c;合理地对数据进行分区可以提高查询性能和数据管理效率。 本文将详细介绍在Postgresql中对空间数据进行表分区的实践过程。 测试计算机容量有限&#xff0c;测试最大数据量为1,000,000条。 关键字: Postgresql PostGIS 表分区 空间数据 测试计算…

Easy Excel合并单元格情况简单导入导出

需求 实现报表数据的导入导出&#xff0c;表格中部分数据是系统生成&#xff0c;部分数据是甲方填写&#xff0c;录入系统。 批号唯一 Maven <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.…

【modbus协议】libmodbus库移植基于linux平台

文章目录 下载库函数源码编译路径添加libmodbus 源码分析核心数据结构常用接口函数 开发 TCP Server 端开发TCP Client 端 下载库函数源码 编译路径添加 libmodbus 源码分析 核心数据结构 modbus_t结构体&#xff1a; 这是 libmodbus 的核心数据结构&#xff0c;代表一个 Mod…

机房巡检机器人有哪些功能和作用

随着数据量的爆炸式增长和业务的不断拓展&#xff0c;数据中心面临诸多挑战。一方面&#xff0c;设备数量庞大且复杂&#xff0c;数据中心内服务器、存储设备、网络设备等遍布&#xff0c;这些设备需时刻保持良好运行状态&#xff0c;因为任何一个环节出现问题都可能带来严重后…

从0到1学习node.js(express模块)

文章目录 Express框架1、初体验express2、什么是路由3、路由的使用3、获取请求参数4、电商项目商品详情场景配置路由占位符规则5、小练习&#xff0c;根据id参数返回对应歌手信息6、express和原生http模块设置响应体的一些方法7、其他响应设置8、express中间件8.1、什么是中间件…

如何搭建直播美颜SDK平台的最佳实践?美颜API的实现与集成详解

本篇文章&#xff0c;将从技术实现、平台搭建、API集成以及性能优化四个方面&#xff0c;为开发者详解如何搭建一个直播美颜SDK平台。 一、直播美颜SDK平台的技术架构 一般的美颜效果包括磨皮、亮肤、瘦脸、大眼等&#xff0c;这些效果的实现需要依赖图像增强和滤镜算法。核心…

【51单片机】第一个小程序 —— 点亮LED灯

学习使用的开发板&#xff1a;STC89C52RC/LE52RC 编程软件&#xff1a;Keil5 烧录软件&#xff1a;stc-isp 开发板实图&#xff1a; 文章目录 单片机介绍LED灯介绍练习创建第一个项目点亮LED灯LED周期闪烁 单片机介绍 单片机&#xff0c;英文Micro Controller Unit&#xff0…

创建ODBC数据源SQLConfigDataSource函数的用法

网络上没有这个函数能实际落地的用法说明&#xff0c;我实践后整理一下&#xff1a; 1.头文件与额外依赖库&#xff1a; #include <odbcinst.h> #pragma comment(lib, "legacy_stdio_definitions.lib") 2.调用函数&#xff1a; if (!SQLConfigDataSourceW(…

阿里云镜像源无法访问?使用 DaoCloud 镜像源加速 Docker 下载(Linux 和 Windows 配置指南)

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f343; vue-uniapp-template &#x1f33a; 仓库主页&#xff1a; GitCode&#x1f4ab; Gitee &#x1f…

java :String 类

在我们之前的讲解中我们已经了解了很多的Java知识&#xff0c;这节我们讲Java中字符如何定义以及关于String如何使用还有常见的string函数。 【本节目标】 1. 认识 String 类 2. 了解 String 类的基本用法 3. 熟练掌握 String 类的常见操作 4. 认识字符串常量池 5. 认识 …

江协科技STM32学习- P21 ADC模数转换器

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

基于SpringCloud的WMS管理系统源码

商品管理&#xff1a;商品类型&#xff0c;规格&#xff0c;详情等设置。 采购管理&#xff1a;采购单录入。 销售管理&#xff1a;销售单录入。 库存管理&#xff1a;库存查询、库存日志 采用前后端分离的模式&#xff0c;微服务版本前端 后端采用Spring Boot、Spring Cl…

python实现放烟花效果庆祝元旦

马上就要2025年元旦啦&#xff0c;提前祝大家新年快乐 完整代码下载地址&#xff1a;https://download.csdn.net/download/ture_mydream/89926458

vLLM推理部署Qwen2.5

vLLM vLLM 是一个用于大模型推理的高效框架。它旨在提供高性能、低延迟的推理服务&#xff0c;并支持多种硬件加速器&#xff0c;如 GPU 和 CPU。 vLLM 适用于大批量Prompt输入&#xff0c;并对推理速度要求高的场景&#xff0c;吞吐量比HuggingFace Transformers高10多倍。 …

手指关节分割系统:视觉算法突破

手指关节分割系统源码&#xff06;数据集分享 [yolov8-seg-C2f-RFAConv&#xff06;yolov8-seg-fasternet-bifpn等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Glob…

灵动AI:艺术与科技的融合

灵动AI视频官网地址&#xff1a;https://aigc.genceai.com/ 灵动AI 科技与艺术的完美融合之作。它代表着当下最前沿的影像技术&#xff0c;为我们带来前所未有的视觉盛宴。 AI 视频以强大的人工智能算法为基石&#xff0c;能够自动分析和理解各种场景与主题。无论是壮丽的自然…

网络学习/复习2套接字

LinuxCode/code26 zc/C语言程序学习 - 码云 - 开源中国

c语言中整数在内存中的存储

整数的二进制表示有三种&#xff1a;原码&#xff0c;反码&#xff0c;补码 有符号的整数&#xff0c;三种表示方法均有符号位和数值位两部分&#xff0c;符号位都是用‘0’表示“正&#xff0c;用1表示‘负’ 最高位的以为被当作符号位&#xff0c;剩余的都是数值位。 整数…

python 制作 发货单 (生成 html, pdf)

起因&#xff0c; 目的: 某个小店&#xff0c;想做个发货单。 过程: 先写一个 html 模板。准备数据&#xff0c; 一般是从数据库读取&#xff0c;也可以是 json 格式&#xff0c;或是 python 字典。总之&#xff0c;是数据内容。使用 jinja2 来渲染模板。最终的结果可以是 h…

使用 telnet 连接 dubbo 服务调用暴露的 dubbo 接口

目录 前言 环境准备 Telnet客户端 zookeeper pom 配置文件 dubbo接口 telnet连接dubbo dubbo命令 ls invoke 前言 工作中的微服务项目远程调用使用的技术是 dubbo&#xff0c;当对外提供了一个 duboo 接口时&#xff0c;无论是开发阶段自测&#xff0c;还是上线了服…