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.mzph.cn/news/883467.shtml

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

相关文章

H5实现PDF文件预览,使用pdf.js-dist进行加载

H5实现PDF文件预览&#xff0c;使用pdf.js-dist进行加载 一、应用场景 在H5平台上预览PDF文件是在原本已经开发完成的系统中新提出的需求&#xff0c;原来的系统业务部门是在PC端进行PDF的预览与展示&#xff0c;但是现在设备进行了切换&#xff0c;改成了安卓一体机进行文件…

基于neo4j的课程资源生成性知识图谱

你是不是还在为毕业设计苦恼&#xff1f;又或者想在课堂中进行知识的高效管理&#xff1f;今天给大家分享一个你一定会感兴趣的技术项目——基于Neo4j的课程资源生成性知识图谱&#xff01;&#x1f4a1; 这套系统通过知识图谱的形式&#xff0c;将课程资源、知识点和学习路径…

前端页面样式没效果?没应用上?

当我们在开发项目时会有很多个页面、相同的标签&#xff0c;也有可能有相同的class值。样式设置的多了&#xff0c;分不清哪个是当前应用的。我们可以使用网页的开发者工具。 在我们开发的网页中按下f12或&#xff1a; 在打开的工具中我们可以使用元素选择器&#xff0c;单击我…

渗透测试-百日筑基—SQL注入篇时间注入绕过HTTP数据编码绕过—下

day8-渗透测试sql注入篇&时间注入&绕过&HTTP数据编码绕过 一、时间注入 SQL注入时间注入&#xff08;也称为延时注入&#xff09;是SQL注入攻击的一种特殊形式&#xff0c;它属于盲注&#xff08;Blind SQL Injection&#xff09;的一种。在盲注中&#xff0c;攻击…

基于丑萌气质狗--C#的sqlserver学习

#region 常用取值 查询List<string> isName new List<string> { "第一", "第二", "第三", "第四" }; List<string> result isName.Where(m > m "第三").ToList();MyDBContext myDBnew MyDBContext(…

web3对象如何连接以太网络节点

实例化web3对象 当我们实例化web3对象&#xff0c;我们一般开始用本地址&#xff0c;如下 import Web3 from web3 var web3 new Web3(Web3.givenProvider || ws://localhost:5173)我们要和以太网进行交互&#xff0c;所以我们要将’ws://localhost:5173’的本地地址换成以太…

如何在短时间内入门并掌握深度学习?

如何在短时间内快速入门并掌握深度学习&#xff0c;是很多读者的困惑——晦涩难懂的数学 知识、复杂的算法、烦琐的编程……深度学习虽然让无数读者心怀向往&#xff0c;却也让不少人望而生畏&#xff0c;深感沮丧&#xff1a;时间没少花&#xff0c;却收效甚微。 如何才能更好…

python对文件的读写操作

任务:读取文件夹下的批量txt数据&#xff0c;并将其写入到对应的word文档中。 txt文件中包含&#xff1a;编号、报告内容和表格数据。写入到word当中&#xff1a;编号、报告内容、表格数据、人格雷达图以及对应的详细说明&#xff08;详细说明是根据表格中的标识那一列中的加号…

设计模式(二)工厂模式详解

设计模式&#xff08;二&#xff09;工厂模式详解 简单工厂模式指由一个工厂对象来创建实例,适用于工厂类负责创建对象较少的情况。例子&#xff1a;Spring 中的 BeanFactory 使用简单工厂模式&#xff0c;产生 Bean 对象。 工厂模式简介 定义&#xff1a;工厂模式是一种创建…

js构造函数和原型对象,ES6中的class,四种继承方式

一、构造函数 1.构造函数是一种特殊的函数&#xff0c;主要用来初始化对象 2.使用场景 常见的{...}语法允许创建一个对象。可以通过构造函数来快速创建多个类似的对象。 const Peppa {name: 佩奇,age: 6,sex: 女}const George {name: 乔治,age: 3,sex: 男}const Mum {nam…

pytorch的标签平滑介绍

什么是标签平滑(Label Smoothing)? 标签平滑(Label Smoothing)是一种正则化技术,旨在防止模型过度自信(即输出的概率分布过于“尖锐”)。在分类任务中,标准的目标标签是one-hot编码,也就是正确类别的概率为 1,其他类别的概率为 0。而标签平滑通过将正确类别的概率从…

小程序开发实战:PDF转换为图片工具开发

目录 一、开发思路 1.1 申请微信小程序 1.2 编写后端接口 1.3 后端接口部署 1.4 微信小程序前端页面开发 1.5 运行效果 1.6 小程序部署上线 今天给大家分享小程序开发系列&#xff0c;PDF转换为图片工具的开发实战&#xff0c;感兴趣的朋友可以一起来学习一下&#xff01…

基于Springboot无人驾驶车辆路径规划系统(源码+定制+开发)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

Hadoop:yarn的Rust API接口

今天头一次接触了yarn的Rust API接口&#xff0c;在本地搭建了集群&#xff0c;能够得到每个任务的详细信息。 (一)得到所有任务的所有信息命令&#xff1a; 默认是json格式&#xff0c;也可以指定xml的格式&#xff0c;如(curl --compressed -H "Accept: application/x…

【大模型理论篇】主流大模型的分词器选择及讨论(BPE/BBPE/WordPiece/Unigram)

1. 背景分析 分词是将输入和输出文本拆分成更小单位的过程&#xff0c;使得大模型能够处理。token可以是单词、字符、子词或符号&#xff0c;取决于模型的类型和大小。分词可以帮助模型处理不同的语言、词汇和格式&#xff0c;并降低计算和内存成本。分词还可以通过影响token的…

fmql之Linux RTC

模拟i2c&#xff0c;连接rtc芯片。 dts&#xff1a; /{ // 根节点i2c_gpio: i2c-gpio {#address-cells <1>;#size-cells <0>;compatible "i2c-gpio";// MIO56-SDA, MIO55-SCL // 引脚编号gpios <&portc 2 0&portc 1 0 >;i2c-gp…

Modbus TCP报错:Response length is only 0 bytes

问题描述&#xff1a; 使用modbus_tk库&#xff0c;通过Modbus tcp连接PLC时&#xff0c;python中的一个报错信息&#xff1a; Response length is only 0 bytes报错原因&#xff1a; 与Modbus TCP 服务端建立连接后没有断开&#xff0c;继续作为长连接使用&#xff0c;客户端…

随笔—git操作

1. 创建一个 GitHub 仓库 登录到 GitHub。点击右上角的 “” 按钮&#xff0c;然后选择 “New repository”。填写仓库名称和描述&#xff0c;选择是否公开&#xff0c;最后点击 “Create repository”。 2. 在本地初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; 在…

【Lammps】atomsk安装与环境变量设置(Linux环境)

【Lammps】atomsk安装与环境变量设置&#xff08;Linux环境&#xff09; 官网配置环境变量测试 官网 https://atomsk.univ-lille.fr/dl.php 下载的安装包如下&#xff1a; 使用Linux的解压命令进行解压&#xff1a; tar -xzvf file.tar.gz注意&#xff1a;file.tar.gz 替换…

【Vue 3】最全组件设计指南:从基础到进阶

&#x1f9d1;‍&#x1f4bc; 一名茫茫大海中沉浮的小小程序员&#x1f36c; &#x1f449; 你的一键四连 (关注 点赞收藏评论)是我更新的最大动力❤️&#xff01; &#x1f4d1; 目录 &#x1f53d; 前言1️⃣ 组件的基础概念与构建2️⃣ 组件通信的核心技术3️⃣ 组件的生命…