[C++]vector模拟实现
目录
前言:
1. vector结构
2. 默认成员函数
2.1 构造函数
无参构造:
有参构造:
有参构造重载:
2.2 赋值运算符重载、拷贝构造(难点)
2.3 析构函数:
3. 扩容
3.1 reserve
3.2 resize
4. 插入删除
5. 迭代器操作
前言:
本篇文章模仿的vector与STL源码并不完全一致,例如本文直接通过new来开辟空间,但是源码中通过内存池分配,但是这并不影响彼此之间的关系,所以本篇文章还是有一定的学习意义的。
1. vector结构
模板:
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
}
我相信如果大家用过vector的话,一定是知道每一次使用vector都需要标注清楚这个类是用来存储什么样的数据的,例如:
vector<int> vv1; 存整型数据
vector<double> vv2; 存浮点型数据
vector<vector<int>> vv3; 存存整型数据的vector数据
所以,我们模拟的vector类就不能单独面向某一种数据,而是应该考虑到所有的类型,就算是一直自定义类型嵌套也能实例化出来,如下:
vector<vector<vector<vector<vector<int>>>>> vv4;
虽然上述的类型对于我们来说估计这辈子都不会遇到,但是我们总不能防止某些好奇心重的小伙伴搞事,所以是必须要能够支持这种写法的,就像是为了满足到酒吧点炒饭的帅小伙。
所以首先得出结论:我们的类需要被构建成模板类。
成员变量:
上面代码中可以看到我们的成员变量的类型是自定义类型重定义iterator,也就是平时我们熟知的迭代器,我们的迭代器实现又是指针的方式,所以可以将这三个变量理解为我们存储数据空间的三个位置。_start对应开头,_finish对应数据结尾,_end_of_storage对应空间容量的最后位置。也即是对应我们顺序表的size和capacity。
2. 默认成员函数
每当我们实现一个类,默认成员函数是必不可少的,特别是像是我们vector这样需要向堆申请空间的类。
2.1 构造函数
无参构造:
无参构造可以说是非常简单了,我们甚至连空间都不需要开辟,只需要通过初始化列表为我们的三个迭代器变量初始化即可,如下:
//默认构造方式,全指针都置空值,后续无论怎么插入都有扩容为指针赋值
vector():_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{}
可能有朋友要问,那用这种方式,不就表示了自己之后不能插入数据之类的吗?事实不是,因为我们还有其它的函数接口为我们服务,大家看下去就明白了。
有参构造:
有参构造咱们就与库保持一次,既然它不支持通过一个值直接去初始化,而支持通过n个T类型数据去构造,那么我们也这样学习它这样:
看不懂上面的库实现方式没事,看我的也是一样:
//有参构造n个T类型的数据进入vector<int>
vector(size_t n, const T& va = T()):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{T* temp = new T[n];for (size_t i = 0; i < n; ++i){//这里赋值操作是<T>和<T>之间的操作temp[i] = va;}_start = temp;_finish = _start + n;_end_of_storage = _finish;
}
这里的构造也是一样,需要通过初始化列表先将三个迭代器变量初始化空指针,因为我们不能保证有没有小聪明蛋给n赋值为0去初始化,其次就是我这里使用了T()这样的匿名对象作为缺省参数。
有同学在这里就要问了:T()这样的匿名对象生命周期不是只有一行吗?你这代码都多少行了,还在用不是错的吗?
其实不是,在C++当中,当我们用const加引用类型的变量去接收匿名对象,会改变匿名对象的生命周期为这个变量的生命周期长度,如下:
const int& ret = T(); 改变T()的生命周期为ret
T(); 生命周期只有一行
值得注意的是上面的const是必须加的,因为我们的T()属于本身是属于临时对象的,而临时对象又有常量属性,也就是不可更改属性。如果不用const接收,那么就会导致错误。
有点意思的是,不加const在vs2013及其之前的版本都是不报错的,后续版本我不清楚,但是到了vs2019这个BUG就被修复了。
函数设计:
这里的代码我用了最全面的写法,后面可以通过函数复用的方式直接究极简化。
首先通过new向堆申请n个T类型的空间,这里不能使用malloc这种C语言的申请方式,原因我相信大家也明白,就是因为我们要实现模板类,那必然有可能使用自定义类型,有自定义类型要就要去调用它的构造函数,只有new才能实现,malloc不行。
T* temp = new T[n];
然后通过循环不断的赋值,最后为三个迭代器变量定位即可,这里循环体中的赋值操作涉及了很复杂的嵌套操作,绝不是简单的一行代码,等到下一小节重载赋值运算符我再来讲。
有参构造重载:
vector(int n, const T& va = T()):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{while (n--){push_back(va);}
}
该重载的函数体就是我所说的究极优化的方式,通过尾插函数,不断的复用,就实现了函数功能,但是重点不在这里,我重载这个函数的意义在于为我们的迭代器区间构造函数服务。
为什么这么说?请先看迭代器区间构造函数。
template<class InputIterator>
vector(InputIterator start,InputIterator end):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{while (start != end){push_back(*start);++start;}
}
首先我们得知道一个点,那就是C++为我们提供了函数重载,那么编辑器就会通过传入的参数类型为我们匹配最适合的函数。
举例:当我们通过下列方式传参:
vector<int> vv1(5,5);
编辑器会将我们的传参类型认为是int,int,而不是size_t,int,也就是说,编辑器就会找到迭代器区间构造函数中去了,这并不是我们想要的结果,而编辑器却认为这是最合适的函数,为了避免这种情况的出现,所以需要重载int型的构造函数。
当然,有的小伙伴可能不太理解为什么我们需要再写一个模板出来,这理解起来真难受。这确实让人很难受,但是他带来的好处也是很大的,因为这样实现的功能,让我们的构造方式做种多样的起来,无论是何种形式的迭代器去构造,我们都有对应的方式去构造出来,这就是牛逼之处。
2.2 赋值运算符重载、拷贝构造(难点)
代码:
vector<T>& operator=(const vector<T>& va)
{if (_start != va._start){//只能通过new的方式实现,因为要调用自定义类型的构造函数T* temp = new T[va.capacity()];//保留数据//memcpy(temp, va._start, sizeof(T)* va.size());int len = va.size();for (int i = 0; i < len;++i){//嵌套temp[i] = va[i];}//释放掉原来空间,如果有的话delete[] _start;_start = temp;_finish = _start + va.size();_end_of_storage = _start + va.capacity();}return *this;
}
赋值需要相同类型才能成功,这里用const vector<T>&相信大家也能理解,我就直接切入重点了。
为什么我代码中要用循环一个一个的赋值,为什么不直接调用库函数memcpy()呢?简单又方便,而是通过循环一个一个的赋值,感觉写得好搓哦。其实呢,也不是博主不想用memcpy(),而是现实给了博主一巴掌,让我认清,写模板类的深拷贝有多令人头疼。
举一个例子:如果我们构建的vector<vector<int>>类型的类,那么我们开辟了一片空间用于存n个vector<int>,然后这些vector<int>被我们直接用memcpy直接拷贝过来了,但是我们呢,又不能直接为单独写一个函数,因为这是模板,要是单独写了,又不满足vector<int>类型了。下图:
原本我们是希望赋值出来的对象能有一个新的空间去承载这些数据,如上图,但是如果我们通过memcpy来实现能够成功吗?请看下图:
很明显,如果通过memcpy实现了一个什么功能?我们确实实现了一层深拷贝,将vector<int>用新的空间承载起来了,但是还有更里面的空间呢?这些空间也是需要new出来的哇。用memcpy还能行吗?能行,但不完全行,除非你保证你以后不会用自定义类型模板。
所以决定采用循环的方式,一次一次的赋值,其中有一句话非常重要,那就是:
temp[i] = va[i];
这一句话没有你想象的那么简单,你想,如果是内置类型我们关不关心他申请空间?不关心,那么对于自定义类型它会怎么做?嵌套!!!,直到遇到内置类型嵌套就结束了,如下图:
上图中,我们可以看到,每次我们赋值拷贝,如果遇到了一个赋值运算符,编辑器就回去检查是否是自定义类型,需要去调用赋值运算符重载函数,如此反复,直到没有赋值运算符重载函数,也就是没有需要申请空间的必要了。
下图的编译,运行,使用也不会报任何的错误了。
拷贝构造:
//拷贝构造
vector(const vector<T>& va)
{//赋值重载*this = va;
}
拷贝构造直接复用赋值运算符重载函数就行,写法和逻辑思维差不多,博主也想偷懒。
2.3 析构函数:
//析构函数
~vector()
{//释放空指针不会出错,无论是什么方式delete[] _start;_start = nullptr;_finish = nullptr;_end_of_storage = nullptr;
}
析构函数也没有什么好讲的,看代码就行。
3. 扩容
3.1 reserve
代码:
void reserve(size_t n)
{if (n > capacity()){size_t len = size();T* temp = new T[n];if (_start != nullptr){//memcpy(temp, _start, sizeof(T) * size());for (size_t i = 0; i < len; ++i){temp[i] = _start[i];}delete[] _start;}_start = temp;_finish = _start + len;_end_of_storage = _start + n;}
}
vector和其它string、顺序表或者说任何数据结构一样,能不缩容就不缩容,所以,当我们的n小于容量大小的时候,不做任何处理直接返回即可。
同样,这里也涉及到了多次深拷贝的问题,也就不能用memcpy来实现,需要用循环依次赋值。还有,咱们需要考虑到当空间异地扩容之后是需要将原空间释放的,以防内存泄漏。
3.2 resize
代码:
void resize(size_t n,const T val& = T())
{//小于操作不需要缩容,只需要将_finish重定位即可if (n < size()){_finish = _start + n;}else if (n == size()){} //无动作else{int len = n - size();//_finish和_end_of_storage的绝对位置之差与n的大小之比if (_finish + n > _end_of_storage){reserve(capacity() == 0 ? n : capacity()+n);}//补齐T类型数据while (len-- > 0){*_finish = val;_finish++;}}
}
resize功能比reserve多一个,那就是能够实现重定size大小,大于size的部分通过数据val占位。然后复用reserve,如果是第一次扩容,那就需要用三目运算来判断给定大小。
4. 插入删除
内容比较简单,相信大家配合注解能够看懂,直接上代码:
//尾插只需要考虑如果空间不够就应该扩容
//并且,还有空容量的情况也需要考虑
void push_back(const T& val)
{if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}//如果不需要reserve表示不用扩容,那么就是原地扩,不需要动_start和_end_of_storage//如果扩了容,reserve会为我们将这几个变量重定向*_finish = val;++_finish;
}//插入
iterator insert(iterator pos, const T& val)
{//检查插入位置的有效性assert(pos >= _start);assert(pos <= _finish);//提前保留绝对距离size_t len = pos - _start;//判断扩容if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}//通过绝对值偏移重定向pos = _start + len;//指向最后一个位置的下一个地址iterator end = _finish;//移动完pos位置的数据,结束while (end > pos){*(end) = *(end - 1);--end;}//因为有赋值运算符重载,那么不管是否是内置类型,都能满足*pos = val;//向后移动一位++_finish;return pos;
}//删除
void erase(iterator pos)
{//判断pos可行性assert(pos >= _start);assert(pos < _finish);//从pos下一个位置地址开始依次向前移动iterator start = pos + 1;//中途是没有任何扩容缩容的地方,所以迭代器不会更换while (start != _finish){*(start - 1) = *start;++start;}//删除会有机会产生结果不确定的意外情况//所以,任何一次删除,该迭代器都应该被销毁//该函数才不会设置返回值--_finish;
}//尾删,检查是否还有数据能删除
void pop_back()
{assert(_start != _finish);--_finish;
}
5. 迭代器操作
//迭代器
iterator begin()
{return _start;
}
iterator end()
{return _finish;
}
const_iterator begin() const
{return _start;
}
const_iterator end() const
{return _finish;
}//求个数、容量
size_t size() const
{return _finish - _start;
}
size_t capacity() const
{return _end_of_storage - _start;
}//重载[],需要区分const有无的区别和pos位置的准确性
T& operator[](size_t pos)
{assert(pos < size());return _start[pos];
}
const T& operator[](size_t pos) const
{assert(pos < size());return _start[pos];
}
迭代器是有失效的情况的,这部分根据不同的编辑器会有不同的实现方式,博主这里不太想多讲,不过我建议大家通过迭代器插入删除数据之后就别再使用这个迭代器了,或者是重新给他定位,防止非法越界。
以上就是博主对vector模拟实现的全部理解了,希望能够帮到大家。
相关文章:

[C++]vector模拟实现
目录 前言: 1. vector结构 2. 默认成员函数 2.1 构造函数 无参构造: 有参构造: 有参构造重载: 2.2 赋值运算符重载、拷贝构造(难点) 2.3 析构函数: 3. 扩容 3.1 reserve 3.2 resize…...

DevOps实战50讲-(2)Jenkins配置
1. Docker镜像方式安装拉取Jenkins镜像docker pull jenkins/jenkins编写docker-compose.ymlversion: "3.1" services:jenkins:image: jenkins/jenkinscontainer_name: jenkinsports:- 8080:8080- 50000:50000volumes:- ./data/:/var/jenkins_home/首次启动会因为数据…...

LC-1599. 经营摩天轮的最大利润(贪心)
1599. 经营摩天轮的最大利润 难度中等39 你正在经营一座摩天轮,该摩天轮共有 4 个座舱 ,每个座舱 最多可以容纳 4 位游客 。你可以 逆时针 轮转座舱,但每次轮转都需要支付一定的运行成本 runningCost 。摩天轮每次轮转都恰好转动 1 / 4 周。…...

Umi使用百度地图服务
需求描述 需要在前端页面中使用地图定位功能,所以在前端umi项目中使用百度地图服务,由于umi项目默认没有入口的html文件,所以无法通过常规的在head中加入外链js的方式使用 百度ak zyqeLCzvQPCCNImRu9yRGOqWlEUicxxGreact使用百度api 链接:…...

js中getBoundingClientRect()方法
getBoundingClientRect()返回值是一个 DOMRect 对象,是包含整个元素的最小矩形(包括 padding 和 border-width)。该对象使用 left、top、right、bottom、x、y、width 和 height 这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 …...

Unity记录2.2-动作-动画、相机、Debug与总结
文章首发及后续更新:https://mwhls.top/4453.html,无图/无目录/格式错误/更多相关请至首发页查看。 新的更新内容请到mwhls.top查看。 欢迎提出任何疑问及批评,非常感谢! 汇总:Unity 记录 摘要:重写了动画触…...

分享十个前端Web3D可视化框架附地址
Three.js:Three.js是一个流行的3D库,提供了大量的3D功能,包括基本几何形状、材质、灯光、动画、特效等。它是一个功能强大、易于使用的框架,广泛用于Web3D可视化应用程序的开发。Three.js:https://threejs.org/Babylon…...

基于WSL2和Clion搭建Win下C开发环境
系列文章目录 一、基于WSL2和Clion搭建Win下C开发环境 二、make、makeFile、CMake、CMakeLists的使用 三、全面、详细、通俗易懂的C语言语法和标准库 文章目录系列文章目录前言WSL2安装WSL常用命令VSCode连接WSLroot密码以systemd启动配置sshClion结语前言 Win下C语言开发环境…...

考研第一天,汤家凤基础班,连续与极限复习笔记
函数连续极限性质保号性证明极值点:夹逼准则二项式展开根号下,大于一,小于一的讨论直接放缩求和分子分母齐次,且分母大一次,用积分单调有界存在极限几个重要的切线放缩证明有界,然后放缩求单调证明有界&…...

聊一聊代码重构——关于变量的代码实践
提炼变量 其目标是将一个复杂表达式或语句分解成更小的部分,并将其存储在变量中。提高代码可读性和复用性 复杂的表达式 有些时候为了方便我们会把业务处理的逻辑写在一起,如果参与处理的内容较多时我们就会创造出一个非常长且难以理解的表达式。当其他…...

Spring之基于注解方式实例化BeanDefinition(1)
最近开始读Spring源码,读着读着发现里面还是有很多很好玩的东西在里面的,里面涉及到了大量的设计模式以及各种PostProcessor注入的过程,很好玩,也很复杂,本文就是记录一下我学习过程中的主干流程。 在开始我们源码解读…...

【STM32】入门(十四):FreeRTOS-任务
1、简述 FreeRTOS应用程序由一组独立的任务构成。 在任何时间点,应用程序中只能执行一个任务,FreeRTOS调度器负责决定所要执行的任务。 每个任务在自己的上下文中执行,不依赖于系统内的其他任务或 FreeRTOS的调度器本身。 FreeRTOS调度器负责…...

apscheduler 的基本介绍和使用
APScheduler有四大组件: 1、触发器 triggers : 触发器包含调度逻辑。每个作业都有自己的触发器,用于确定下一个任务何时运行。除了初始配置之外,触发器是完全无状态的。 有三种内建的trigger: (1)date: 特定…...

Oracle中merge Into的用法
Oracle中merge Into的用法 使用场景 在操作数据库时,数据存在的情况下,进行update操作;不存在的情况下,进行insert操作;在Oracle数据库中,能够使用merge into来实现。 基本语法 merge into table_name …...

JDK19下载、安装与测试的完整图文教程
一、下载JDK 1、官网获取:https://www.oracle.com/ 1.1 点击“Products”; 1.2 选择“Java”; 1.3 选择“Download Java”; 1.4 选择“Java downloads”,这里以最新版(JDK19)为例ÿ…...

Vector - CAPL - 获取相对时间函数
在自动化开发中,无论是CAN通信测试,还是网络管理测试,亦或是休眠唤醒等等存在时间相关的,都可能会使用相关的时间函数;今天主要介绍的就是获取当前时间,我们知道vector工具的最大优势就是稳定和精确度高&am…...

C++编程语言STL之unordered_map介绍
本文主要介绍 C 编程语言的 STL(Standard Template Library) 中 unordered_map 的相关知识,同时通过示例代码介绍 unordered_map 的常见用法。1 概述C标准库提供了四个无序关联容器(unordered associated container)&a…...

【独家】华为OD机试 - 最快检测效率-核酸(C 语言解题)
最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 最近更新的博客使用说明本期…...

【Redis应用】基于Redis实现共享session登录(一)
🚗Redis应用学习第一站~ 🚩本文已收录至专栏:数据库学习之旅 👍希望您能有所收获 👉相关推荐:使用短信服务发送手机验证码进行安全校验 一.引入 在开发项目过程中,我们常常能碰到需要登录注…...

Android framework系列2 - Init进程
1、源码 入口:system/core/init/main.cpp2 流程图 https://note.youdao.com/s/EtnCswft 3、代码详解 主入口共三步,如流程图所示,我们主要看下最后一步 入口在init.cpp下,这个阶段主要来解析init.rc并执行此文件下的命令 看到…...

2023年“网络安全”赛项江苏省淮安市选拔赛 任务书
任务书 一、竞赛时间 共计3小时。 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段单兵模式系统渗透测试 任务一 服务器内部信息获取 任务二 网站渗透测试 任务三 Linux系统渗透提权 任务四 Web渗透测试 第二阶段分组对抗 备战阶段 攻防对抗准备工作 系统加…...

2023年Wireshark数据包分析——wireshark0051.pcap
Wireshark数据包分析 任务环境说明: 服务器场景:FTPServer220223服务器场景操作系统:未知(关闭连接)FTP用户名:wireshark0051密码:wireshark0051从靶机服务器的FTP上下载wireshark0051.pcap数据包文件,找出黑客获取到的可成功登录目标服务器FTP的账号密码,并将黑客获…...

SpringMVC的自定义配置和自动化配置
SpringBoot的自动配置MVC处理加载逻辑基于Spring Boot的MVC自动化配置由WebMvcAutoConfiguration类完成,部分关键源码:AutoConfiguration(after { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,ValidationAutoConf…...

画图说透 ZooKeeper如何保证数据一致性:选举和ZAB协议
1、zookeeper是什么? zookeeper能被各个牛逼的中间件项目中所依赖,已经说明了他的地位。一出手就是稳定的杀招。zookeeper是什么?官网中所说,zookeeper致力于开发和维护成为一个高度可靠的分布式协调器。 开局一张图,…...

错误异常捕获
1、React中错误异常捕获 在 React 中,可以通过 Error Boundaries(错误边界)来捕获错误异常。Error Boundaries 是一种 React 组件,它可以在其子组件树的渲染期间捕获 JavaScript 异常,并且可以渲染出备用 UI。React 提…...

js垃圾回收机制
内存的生命周期 ]S环境中分配的内存,一般有如下生命周期 1.内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存 2.内存使用:即读写内存,也就是使用变量、函数等 3.内存回收: 使用完毕,由垃圾回收器自动回收不再…...

YApi分析从NoSQL注入到RCE远程命令执行.md
0x00 前提 这个是前几个月的漏洞,之前爆出来发现没人分析就看了一下,也写了一片 Nosql注入的文章,最近生病在家,把这个写一半的完善一下发出来吧。 0x01 介绍 YApi是一个可本地部署的、打通前后端及QA的、可视化的接口管理平台…...

【C++】stl_list介绍和实现,list和vector区别,list vector string 迭代器失效
本篇博客详细介绍list的实现&细节讲解,并且在文章末对list和vector,string进行区分和复习 list的基本结构就是双向带头循环链表,链表和顺序表的差别我们在前面数据结构的时候早就学过了,不再赘述 在使用stl库里面list时&…...

linux-kernel-ecmp-ipv4
当使用ip route add/del添加或者删除路由时,通过触发netlink发送信息到各协议路由系统注册的netlink处理函数,如add时调用函数为inet_rtm_newroute。Equal Cost Multi Path,在ip交换网络中存在到达同一目的地址的多条不同的路径,而且每条路径…...

蒙特卡洛树搜索(MTCS)
一、目标 一种启发式的搜索算法,在搜索空间巨大的场景下比较有效 算法完成后得到一棵树,这棵树可以实现:给定一个游戏状态,直接选择最佳的下一步 二、算法四阶段 1、选择(Selection) 父节点选择UCB值最…...