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

模拟实现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出逃计划&#xff01;我们应如何看待ChatGPT前言 好久没有更新过技术文章了&#xff0c;这个周末听说了一个非常火的技术ChatGPT 4.0&#xff0c;于是在闲暇之余我也进行了测试&#xff0c;今天这篇文章就给大家介绍一…...

Java反射(详细学习笔记)

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

学习 Python 之 Pygame 开发魂斗罗(十二)

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

Linux下字符设备驱动开发以及流程介绍

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

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&#xff09;断言结果成功2&#xff09;断言结果失败前言 本文的…...

8大核心语句,带你深入python

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

【批处理】- 批处理自动安装Mysql与Redis

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

聊聊华为的工作模式

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

燕山大学-面向对象程序设计实验-实验6 派生与继承:多重派生-实验报告

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

分割两个字符串得到回文串[抽象--去除具体个性取共性需求]

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

【LeetCode】1609. 奇偶树、1122. 数组的相对排序

作者&#xff1a;小卢 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 1609. 奇偶树 1609. 奇偶树 题目描述&#xff1a; 如果一棵二叉树满足下述几个条件&#x…...

【C++初阶】4. Date类的实现

如果下面博客有不理解的地方&#xff0c;可以查看源码&#xff1a;代码提交&#xff1a;日期类的实现 1. 构造函数的实现 由于系统实现的默认构造函数即便采用默认值的形式也只能存在1个固定的默认日期&#xff08;例如&#xff1a;1997-1-1&#xff09;。所以&#xff0c;构…...

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已经创作一年了。打心底讲&#xff0c;对于在CSDN开始坚持创作的原因&#xff0c;我用一句话来概括最合适不过了——“无心插柳柳成荫” 为什么这么说呢&#xff1f; 这要从我的一篇博客说起——《输入命令Javac报错详解》&#xff1a; 那也是我第一次…...

Jetson Nano驱动机器人的左右两路电机

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

如何通过openssl生成公钥和私钥?

1、生成RSA秘钥的方法 生成RSA秘钥的方法&#xff1a; openssl genrsa -des3 -out privkey.pem 2048 注&#xff1a;建议用2048位秘钥&#xff0c;少于此可能会不安全或很快将不安全。 这个命令会生成一个2048位的秘钥&#xff0c;同时有一个des3方法加密的密码&#xff0c…...

Verilog的If语句和Case语句

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

HJ31 单词倒排

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

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

shell脚本--常见案例

1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件&#xff1a; 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

UE5 学习系列(三)创建和移动物体

这篇博客是该系列的第三篇&#xff0c;是在之前两篇博客的基础上展开&#xff0c;主要介绍如何在操作界面中创建和拖动物体&#xff0c;这篇博客跟随的视频链接如下&#xff1a; B 站视频&#xff1a;s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

spring:实例工厂方法获取bean

spring处理使用静态工厂方法获取bean实例&#xff0c;也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下&#xff1a; 定义实例工厂类&#xff08;Java代码&#xff09;&#xff0c;定义实例工厂&#xff08;xml&#xff09;&#xff0c;定义调用实例工厂&#xff…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

MySQL 知识小结(一)

一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库&#xff0c;分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷&#xff0c;但是文件存放起来数据比较冗余&#xff0c;用二进制能够更好管理咱们M…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…...

华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)

题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...