模拟实现STL容器之vector
文章目录
- 前言
- 1.大体思路
- 2.具体代码实现
- 1.类模板的创建
- 2.构造函数
- 1.无参构造
- 2.拷贝构造 迭代器构造和给定n个val值构造以及析构函数
- 3.空间扩容
- 1.reserve
- 2.resize
- 4.操作符重载
- 1.[ ]重载
- 2.赋值运算符重载
- 5.数据增加和删除
- 1.尾插
- 2.任意位置插入
- 3.任意位置删除
- 4.尾删
- 6.一些其他接口
- 3.总结
前言
本文主要对vector容器的实现进行讲解,vector我们在使用的感觉它有点像数组,它也是个类模板,可以根据需要实例化出不同的模板类用于存储数据。它的底层实现也是像之前实现的顺序表。本文主要参考库中的vector实现,通过模拟实现让我们对容器理解更加深刻。
1.大体思路
这个vector容器底层也和顺序表类似,在实现的时候我们可以先去看看库中源码。这里就不放出库中源码截图了。直接说结论吧:库中的vector类模板主要有以下几个成员:
_start _finish _end_of_storage
这个3个成员变量,有之前实现顺序表的基础相信很容易看出来,_start肯定是指向存储数据首地址的,finish是存储末尾数据位置的后一个位置,end是指向存储数据的空间最后一个位置,也就是相当于capacity。这个3个成员变量是指针类型,这是为了后续实现迭代器比较方便于才这样设计的。大概了解成员变量的组成后,就可以着手实现了。这里我们也仿照库中实现按照类模板的方式去实现。
2.具体代码实现
1.类模板的创建
namespace Ly
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
}
这里我们也用命名空间的方式将其封起来避免和库中vector起冲突。这里的vector底层存储数据的那一套和顺序表类似,可以天然的使用原生指针作为迭代器。这里为了图方便就不走初始化列表了,直接给缺省值。
2.构造函数
构函数这里有很多细节需要注意,有些地方需要复用其他成员函数。这里为了理清逻辑我们先画图分析一下。
具体问题如下图
以string为例
在实现vector的时候会涉及到更深层次的拷贝的,相当于要做两次深拷贝处理,这点需要我们注意
1.无参构造
vector()
: _start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{}
这里的无参构造很简单,不过多的赘述了。我们主要看看下面的几种构造。
2.拷贝构造 迭代器构造和给定n个val值构造以及析构函数
vector(size_t n,const T & val){reserve(n);for (int i = 0; i < n; ++i){push_back(val);}}
这里直接复用reserve进行扩容然后复用尾插插入数据
这两个接口下面会介绍具体实现的,这里先不着急。
vector(const vector<T>& val){_start = new T[val.capacity()];for (size_t i = 0; i < val.size(); i++){_start[i] = val._start[i];}_finish = _start + val.size();_end_of_storage = _start + val.capacity();/*_start = new T[val.capacity()];memcpy(_start, val._start, sizeof(T)*val.size());_finish = _start + val.size();_end_of_storage = _start + val.capacity();*/}
这个拷贝构造我们采用赋值形式的进行数据的拷贝,如果是自定义类型这个赋值会调用后续实现的operator=函数,如果不是自定义类型就是直接赋值和memcpy一样的处理方式,这样就不会出现问题了。
这个_finish和_end_of_storage记得及时更新。至于为啥采用赋值的形式我上面说的很清楚了,这里就不在赘述了。还有需要注意的类名<T>才是类型这点在之前的模板博客中提到过。
// 重新声明迭代器,迭代器区间[first,last)可以是任意容器的迭代器template<class InputIterator>vector(InputIterator first, InputIterator last){while (first != last){push_back(*first);++first;}}
这里迭代器构造的时候需要在定义一个模板参数,这样这个迭代器构造函数可以传任意容器的迭代器作为参数进行构造。
补充一个构造
//避免模板示例化的时候误调上面的迭代器构造vector(int n, const T& val){reserve(n);for (int i = 0; i < n; ++i){push_back(val);}}
理论上将,提供了vector(size_t n, const T& value = T())之后vector(int n, const T& value = T())就不需要提供了,但是对于: vector v(2, 1); 编译器在编译时,认为T已经被实例化为int,而2和1编译器会默认其为int类型 就不会走vector(size_t n, const T& value = T())这个构造方法, 最终选择:vector(InputIterator first, InputIterator last)因为编译器觉得区间构造两个参数类型一致,因此编译器就会将InputIterator实例化为int但是10和5根本不是一个区间,编译时就报错了故需要增加该构造方法
~vector(){delete[] _start;_start = _finish = _end_of_storage = nullptr;}
这里析构函数也很简单,释放空间后直接置空。
3.空间扩容
1.reserve
void reserve(size_t n){ //需要提前之前的size的大小保存起来确定_finsh的位置size_t sz = size();if (n > capacity()){T* tem = new T[n];if (_start){for (size_t i = 0; i < val.size(); i++){_start[i] = val._start[i];}delete[] _start;}_start = tem;_end_of_storage = tem + n;_finish = tem+sz;}}
只有当n大于capccity的时候才会进行扩容处理,
其中扩容的时候会涉及到数据的拷贝所以我们这里还是采用赋值的形式来处理,之前实现的时候是采用memcpy进行直接拷贝上面说了这种拷贝处理是有问题的,
所以采用赋值的形式。这里要加上一个判断,_start为空的时候调用resreve实际上是构造函数调用它进行扩容,这个时候reserve只是单独的扩容而已。还有一点需要注意之前的sz需要保存起来。这为啥需要保存起来呢?我们知道这3个成员变量都是指针,如何确定3个指针的指向呢?_start很容易确定,剩下的两个都可以通过_start来确定,_start+size=_finish,_start+capacity=end_of_storage,然而我们的size和capacity都是通过上述两个成员变量和_start相减来确定的。
假如我们先更新了_start,那么_finish=_start+size 然而size=_finish -_start等价代换之后_finish=_start+_finsh-_start=_finish,相当于_finish没有更新。所以我们需要提前保存sz。
2.resize
void resize(int n, const T& val=T()){ //相当于删除数据if (n < size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _end_of_storage){*_finish = val;++_finish;}}}
这里有一些细节需要注意。关于这个n的3种分类情况我就不多说了,和string类似处理。
我们看到形参是给了缺省值的,我们知道对于内置类型来说是没有构造函数这么一说的那如果T实例化成内置类型怎么办呢,C++中为了配合模板的使用,内置类型也是有构造函数的,但是只是限于在使用模板的时候,还有一点我们看到给的缺省值是个匿名对象,我们知道匿名对象的使用周期很短只有一行,这里为啥还是使用呢?被const加&修饰的匿名对象生命周期会被延长,需要注意的是匿名对象具有常性如果不加const会报错。
4.操作符重载
操作符重载我们的重点放在赋值运算符重载上,这是后续很多地方都需要用到的。
1.[ ]重载
T& operator[](size_t pos){assert(pos < size());return _start[pos];}
[ ]重载支持下标访问这点就不多说了和string类似的处理。
2.赋值运算符重载
//这个是引用作为参数void swap(vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endOfStorage, v._end_of_storage);}//这个是传值传参 不影响实参vector<T>& operator=(vector<T> v){swap(v);return *this;}
我看到这个赋值重载是传值传参不是传引用传参,所以不会影响实参,这个形参是临时变量,但是这个临时变量会调用拷贝构造,所以这个临时变量的存储数据的空间也是独立的,我们在调用swap和这个临时变量进行交换即可,这样就完成了一次赋值操作。交换后实际上影响的是这个临时变量,这样处理极为巧妙。
这里的细节主要在这个swap函数和这个重载函数的形参的传递形式上。
5.数据增加和删除
1.尾插
void push_back(const T& val){if (size()== capacity()){reserve(capacity()==0?4:2* capacity());}*_finish = val;++_finish;}
这里尾插的时候需要注意一下如果capacity为0这个时候需要将它修正为不为0的数,这样才会正常的扩容。如果原有的空间满了我们按照二倍的方式进行扩容。
2.任意位置插入
iterator insert(iterator pos, const T& val){ assert(pos >= _start && pos <= _finish);if (_finish == _end_of_storage){ //防止扩容之后迭代器失效int len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());pos = _start + len;}iterator end = _finish;while (end>pos){*end = *(end - 1);end--;}*pos = val;++_finish;return pos;}
这里有个特别重要的点就是
迭代器失效问题
,这是为啥呢?我们在进行插入的时候会进行扩容,扩容之后这个pos就会变成野指针,扩容的时候原有的空间被释放了重新申请的空间,为了避免这种情况,我们提前记录好pos和_start之间的间距。
剩下的就是挪动数据了,这个挪动数据不用像之前string那样考虑什么无符号整形减的时候会数值会变大。但是注意我们在外部调用这个插入接口的时候,外部的迭代器可能会失效买这个时候需要更新重新接收插入之后的pos值,返回值我们也设置为迭代器类型便于接收新的pos位置。
3.任意位置删除
iterator erase(iterator pos){assert(pos >= _start && pos < _finish);iterator start = pos + 1;while (start!=_finish){*(start - 1) = *start;start++;}--_finish;return pos;}
迭代器都是常用!=来判断遍历的,这里我们也按照这样的方式遍历。
我们考虑一个问题删除的时候迭代器会失效吗?从代码上看删除数据不需要扩容貌似迭代器不会失效,我们来看看如下的情况:
从图我们看出删除的时候也会造成迭代器失效,如果这个图中的it在去访问就是非法操作。vs中对于这点处理的比较严格直接会断言报错,Liunx下有相对宽松一点,程序有时候不会报错。
4.尾删
void pop_back(){assert(!empty());--_finish;}
这里尾插就更简单了_finish–即可,注意删除的时候vector不能为空
6.一些其他接口
iterator begin(){return _start;}iterator end(){return _finish;}
这里迭代器实现就比较简单了,vector的迭代器可以直接用原生指针来实现。这里const的迭代器没有写,这个实现就是把iterator换成const_iterator即可,也很简单
size_t size()const{return _finish - _start;}size_t capacity()const{return _end_of_storage- _start;}bool empty()const{return _start == _finish;}
上面这些接口很简单没啥好说的,看代码就能懂。
3.总结
关于这个vetcor实现,其中最值得注意的几个点是:
1.拷贝构造的时候可能涉及到更深层次的拷贝我们这里不采用memcpy的方式处理,memcpy处理内置类型没啥问题,如果是自定义类型就会出现问题,我们采用赋值的方式进行数据的拷贝,这样对于自定义类型来说会调用拷贝构造,对于内置类型处理方式和memcpy是一样的,因为我们别的构造函数调用了这个尾插,使所以尾插也是使用的直接赋值的形式。2.第二点就是这个赋值重载的时候调用swap,这个形参的使用是个细节需要注意。3.第三点就是迭代器在使用的可能会造成迭代器失效的问题,这点需要考虑清楚,不然会有很严重的问题。所以我们在使用迭代器的时候一定要注意及时更新迭代器。
以上内容如有问题,欢迎指正!
相关文章:

模拟实现STL容器之vector
文章目录前言1.大体思路2.具体代码实现1.类模板的创建2.构造函数1.无参构造2.拷贝构造 迭代器构造和给定n个val值构造以及析构函数3.空间扩容1.reserve2.resize4.操作符重载1.[ ]重载2.赋值运算符重载5.数据增加和删除1.尾插2.任意位置插入3.任意位置删除4.尾删6.一些其他接口3…...

ChatGPT-4.0 : 未来已来,你来不来
文章目录前言ChatGPT 3.5 介绍ChatGPT 4.0 介绍ChatGPT -4出逃计划!我们应如何看待ChatGPT前言 好久没有更新过技术文章了,这个周末听说了一个非常火的技术ChatGPT 4.0,于是在闲暇之余我也进行了测试,今天这篇文章就给大家介绍一…...

Java反射(详细学习笔记)
Java反射 1. Java反射机制概述 Reflection(反射)是java被视为java动态语言的关键,反射机制允许程序在执行期间借助于Reflection API获取任何类的内部信息,并能直接操作任意对象的内部属性及方法。 Class c Class.forName(&quo…...

学习 Python 之 Pygame 开发魂斗罗(十二)
学习 Python 之 Pygame 开发魂斗罗(十二)继续编写魂斗罗1. 修改玩家扣减生命值2. 解决玩家下蹲子弹不会击中玩家而是直接让玩家死亡的问题3. 完善地图4. 增加产生敌人函数,解决一直产生敌人的问题5. 给玩家类增加计算玩家中心的方法继续编写魂…...

Linux下字符设备驱动开发以及流程介绍
文章目录1 - 字符设备介绍2 - 字符设备开发流程图3 - 字符设备开发流程具体讲解(1)设备编号的定义与申请【1】Linux主次设备号介绍【2】分配设备编号【3】释放主次设备号(2)定义file_operations结构体-初始化接口函数(…...

Web自动化框架断言方法实现
前言1、设计用例方法关键字1.1、获取元素属性值2.1、断言2、代码实现2.1、实现获取元素属性值2.1.1 函数实现2.1.2 方法配置2.1.2 用例调试2.1.3 html属性2.2、实现断言2.2.1 函数2.2.2 方法配置2.2.3 用例调试1)断言结果成功2)断言结果失败前言 本文的…...

8大核心语句,带你深入python
人生苦短 我用python 又来给大家整点好东西啦~ 咱就直接开练噜!内含大量代码配合讲解 python 安装包资料:点击此处跳转文末名片获取 1. for - else 什么?不是 if 和 else 才是原配吗? No,你可能不知道, else 是个…...

【批处理】- 批处理自动安装Mysql与Redis
前言 在全新环境中安装MySQL与Redis操作是挺麻烦的,于是就想使用脚本来自动安装,使用批处理进行一步到位的安装,后面还能使用工具进行打包成exe可执行文件,一键安装,最后能够更好的部署项目到windows系统的服务器。 …...

聊聊华为的工作模式
目录 一、试用期与加班工资 二、招聘 三、月度答辩和转正答辩 四、可信考试认证 五、接口人 六、问题缺陷单 七、代码检视 八、功能开发 九、出征海外 一、试用期与加班工资 一般而言,试用期持续的时间为3-6个月,工资、奖金都按正式员工的标准…...

燕山大学-面向对象程序设计实验-实验6 派生与继承:多重派生-实验报告
CSDN的各位友友们你们好,今天千泽为大家带来的是燕山大学-面向对象程序设计实验-实验5 派生与继承:单重派生-实验报告,接下来让我们一起进入c的神奇小世界吧,相信看完你也能写出自己的 实验报告!本系列文章收录在专栏 燕山大学面向对象设计报告中 ,您可以在专栏中找…...

分割两个字符串得到回文串[抽象--去除具体个性取共性需求]
抽象前言一、分割两个字符串得到回文串二、双指针总结参考文献前言 抽象去个性留共性,是因为具体个性对于解决问题是个累赘。少了累赘,直击需求,才能进行问题转换或者逻辑转换。 一、分割两个字符串得到回文串 二、双指针 // 限定死了&…...

【LeetCode】1609. 奇偶树、1122. 数组的相对排序
作者:小卢 专栏:《Leetcode》 喜欢的话:世间因为少年的挺身而出,而更加瑰丽。 ——《人民日报》 1609. 奇偶树 1609. 奇偶树 题目描述: 如果一棵二叉树满足下述几个条件&#x…...

【C++初阶】4. Date类的实现
如果下面博客有不理解的地方,可以查看源码:代码提交:日期类的实现 1. 构造函数的实现 由于系统实现的默认构造函数即便采用默认值的形式也只能存在1个固定的默认日期(例如:1997-1-1)。所以,构…...

ES6新特性--变量声明
可以使用let关键字来声明变量let a;let b,c;//同时声明多个变量let stu = 张三;let name =李四,age = 12;//声明变量的同时赋值 let关键字使用的注意事项(1).变量在声明的时候不可以重复,这也符合其他语言的变量声明规范 let name = 李四; let name = 张三;//这里开始报错,但…...

【Django】缓存机制
文章目录缓存的介绍Django的6种缓存方式开发调试缓存dummy.DummyCache内存缓存locmem.LocMemCache文件缓存filebased.FileBasedCache⭐️数据库缓存db.DatabaseCacheMemcache缓存memcached.MemcachedCacheMemcache缓存memcached.PyLibMCCacheDjango缓存的应用内存缓存cache_pag…...

我的创作纪念日——一年的时间可以改变很多
机缘 不知不觉来到CSDN已经创作一年了。打心底讲,对于在CSDN开始坚持创作的原因,我用一句话来概括最合适不过了——“无心插柳柳成荫” 为什么这么说呢? 这要从我的一篇博客说起——《输入命令Javac报错详解》: 那也是我第一次…...

Jetson Nano驱动机器人的左右两路电机
基于Jetson Nano板子搭建一个无人车,少不了减速电机驱动轮子滚动,那如何驱动呢?从Jetson.GPIO库文件来说,里面没有支持产生PWM的引脚,也就意味着Jetson nano没有硬件产生PWM的能力,所以我们不得不使用别的方…...

如何通过openssl生成公钥和私钥?
1、生成RSA秘钥的方法 生成RSA秘钥的方法: openssl genrsa -des3 -out privkey.pem 2048 注:建议用2048位秘钥,少于此可能会不安全或很快将不安全。 这个命令会生成一个2048位的秘钥,同时有一个des3方法加密的密码,…...

Verilog的If语句和Case语句
这篇文章将讨论 verilog 中两个最常用的结构----if语句和case语句。在之前的文章中学习了如何使用过程块(例如always块)来编写按顺序执行的verilog 代码。此外还可以在过程块中使用许多语句----统称为顺序语句,如case 语句和 if 语句。这篇文…...

HJ31 单词倒排
描述 对字符串中的所有单词进行倒排。 说明: 1、构成单词的字符只有26个大写或小写英文字母; 2、非构成单词的字符均视为单词间隔符; 3、要求倒排后的单词间隔符以一个空格表示;如果原字符串中相邻单词间有多个间隔符时…...

leetcode——203.移除链表元素
文章目录🐨1.题目🪅2.解法1-头节点迭代🌿2.1 思路🌿2.2 代码实现🦆3. 解法2-创建新链表🎏3.1 思路🎏3.2 代码实现🐐4. 题目链接🐨1.题目 给你一个链表的头节点head和一个…...

GPT-4来袭:开启人工智能新时代
文章目录介绍GPT4 模型演示示例示例 1示例 2示例 3示例 4示例 5最后Reference介绍 2023年3月15日,OpenAI公司正式发布了先进的自然语言处理模型GPT-4,前不久发布的GPT-3.5模型只能理解文字的语言模型,而新发布的GPT4则是多模态模型ÿ…...

芯微电子IPO终止:业绩开始大幅下滑,王日新、王苟新兄弟不同命
近日,深圳证券交易所披露的信息显示,黄山芯微电子股份有限公司(下称“芯微电子”)申请撤回发行上市申请文件。因此,深圳证券交易所决定终止对其首次公开发行股票并在创业板上市的审核。 据贝多财经了解,芯…...

【C++】用手搓的红黑树手搓set和map
目录 一、set/map的底层结构 1、set/map的源码 2、利用模板区分set/map 3、利用仿函数控制比较大小 二、set/map的迭代器(红黑树的迭代器) 1、红黑树的begin、end迭代器 2、红黑树迭代器的operator 3、红黑树迭代器的operator-- 三、set的const…...

【C++】空指针弃NULL用nullptr
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。声明空指针的3种方法: int* p1 NULL; int* p2 nullptr; int* p3 0; 在C语言中常用NULL生成空指针,NULL是一个宏…...

【selenium学习】数据驱动测试
数据驱动在 unittest 中,使用读取数据文件来实现参数化可以吗?当然可以。这里以读取 CSV文件为例。创建一个 baidu_data.csv 文件,如图所示:文件第一列为测试用例名称,第二例为搜索的关键字。接下来创建 test_baidu_da…...

嵌入式硬件电路设计的基本技巧
目录 1 分模块 2 标注关键参数 3 电阻/电容/电感/磁珠的注释 4 可维修性 5 BOM表归一化 6 电源和地的符号 7 测试点 8 网络标号 9 容错性/兼容性 10 NC、NF 11 版本变更 12 悬空引脚 13 可扩展性 14 防呆 15 信号的流向 16 PCB走线建议 17 不使用\表示取反 不…...

Spring MVC 图片的上传和下载
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...

远程工具神器之MobaXterm (小白必看)
目录 1、介绍 2、ssh连接详解过程 3、特点 1、介绍 带有 X11 服务器、选项卡式 SSH 客户端、网络工具等的 Windows 增强型终端。 MobaXterm 是您远程计算的终极工具箱。在单个Windows应用程序中,它提供了大量功能,这些功能是为程序员,网站管…...

VRIK+Unity XR Interaction Toolkit 实现VR上半身的追踪(附带VRM模型导入Unity方法和手腕扭曲的解决方法)
文章目录📕第一步:配置 OpenXR XR Interaction Toolkit 的开发环境📕第二步:导入人物模型⭐VRM 模型导入 Unity 的方法📕第三步:配置 VRIK⭐给模型加上 VRIK 组件⭐将模型的头部和手部的位置作为 VR 追踪目…...