【c++丨STL】vector模拟实现

🌟🌟作者主页:ephemerals__
🌟🌟所属专栏:C++、STL

目录

前言

一、vector底层刨析

二、模拟实现

1. 属性、迭代器以及函数声明

2. 功能实现

交换两个容器的内容

构造函数

拷贝构造

赋值重载

析构函数 

下标引用operator[ ]

容量接口

迭代器接口

判空

resize

reserve

插入和删除

insert和push_back

erase和pop_back

3. 程序全部代码

总结


前言

        之前我们学习了vector的常用接口及其使用方法:

【c++丨STL】vector的使用-CSDN博客

本篇文章,我们将深入探讨vector的底层实现原理,并尝试模拟实现其结构以及一些常用接口。 

一、vector底层刨析

        之前已经提到,vector的底层是动态顺序表,我们使用c语言实现顺序表的结构时赋予了它三个成员变量:

typedef int SLDataType;//动态顺序表
typedef struct SeqList
{SLDataType* arr;//定义起始指针,后续动态开辟内存空间int size;//有效数据的个数int capacity;//数组的空间大小
}SL;

可以看到,我们定义了一个起始指针指向分配的内存,然后用size和capacity两个变量分别记录元素个数和空间容量。接下来,我们再看看SGI版本的STL源码:

为了提高程序的兼容性和灵活性,源码并没有使用size和capacity等属性,而是使用了三个迭代器(指针)来维护数组。这三个迭代器分别指向数组的不同位置:

start:指向首元素

finish:指向末尾元素的“后一位”

end_of_storage:指向可用空间的末尾

图示:

那么,接下来我们在模拟实现vector时,将仿照SGI版本的做法,通过定义相应的成员变量(即三个迭代器)来构建其内部结构

        另外,由于vector本质上是一个类模板允许我们存储任意类型的数据元素,因此在接下来模拟实现vector的过程中,我们也将采用类模板来进行定义。由于类模板成员函数的定义和声明分离到两个文件会造成链接错误,我们选择在头文件中同时完成这些成员函数的声明与定义

二、模拟实现

1. 属性、迭代器以及函数声明

        首先是属性、迭代器的声明以及我们需要实现的接口:

#include <iostream>
#include <algorithm>
#include <cassert>
using namespace std;template<class T>
class Vector
{
public://vector的迭代器是一个原生指针typedef T* iterator;typedef const T* const_iterator;//迭代器接口iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//无参构造Vector();//初始化器构造Vector(initializer_list<T> l);//迭代器区间构造template<class InputIterator>Vector(InputIterator first, InputIterator last);//n个val值构造Vector(size_t n, const T& val = T());//拷贝构造Vector(const Vector<T>& v);//赋值重载Vector<T>& operator=(Vector<T> v);//析构~Vector();//下标引用T& operator[](size_t i);const T& operator[](size_t i) const;//容量接口size_t size() const;size_t capacity() const;//判空bool empty() const;//交换void swap(Vector<T>& tmp);//调整大小void resize(size_t n, const T& val = T());//增容void reserve(size_t n);//插入和删除void push_back(const T& x);void pop_back();iterator insert(iterator pos, const T& x);iterator erase(iterator pos);
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;
};

      由于vector和string的数据元素都是采用顺序存储的方式,它们在功能实现上存在相似之处。因此,建议先阅读string模拟实现之后,再来阅读本文,否则其中的一些实现过程可能会较难理解。

2. 功能实现

        接下来,我们将正式开始实现上述接口的功能。

交换两个容器的内容

        与之前实现string时相同,直接交换它们的成员变量就可以完成数据的交换,无需重新开辟空间。

代码实现:

//交换
void swap(Vector<T>& tmp)
{std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_end_of_storage, tmp._end_of_storage);
}

构造函数

        这里我们实现了四个构造函数的重载:分别是:无参构造、初始化器构造、迭代器区间构造和n个val值构造

//无参构造
Vector()
{}
//初始化器构造
Vector(initializer_list<T> l)
{reserve(l.size());for (auto& e : l){push_back(e);}
}
//迭代器区间构造
template<class InputIterator>
Vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);first++;}
}
//n个val值构造
Vector(size_t n, const T& val = T())
{reverse(n);for (size_t i = 0; i < n; i++){push_back(val);}
}

不难发现,由于我们在成员变量声明时已经赋初值,所以无参构造中什么都不用做。这样无参构造也可以写成如下形式:

Vector() = default;//强制生成无参构造

我们显示写了构造函数后,编译器就不会默认生成无参构造函数。我们用这条语句就可以强制让编译器生成一个无参的构造函数

        其余的构造函数都是通过遍历来访问每个元素,并将它们依次尾插到数组中的。push_back、reverse等函数我们之后实现。这里特别注意一下n个val值构造函数,如果我们不传val参数,则会调用T类型的默认构造函数,生成一个匿名对象并赋值给val。

拷贝构造

        与初始化器构造的原理相似,我们遍历被拷贝数组的每个元素,并将它们逐一尾插到当前数组中。

//拷贝构造
Vector(const Vector<T>& v)
{reverse(v.capacity());for (auto& e : v){push_back(e);}
}

赋值重载

        这里我们使用新式写法(string使用过),构造新对象然后交换,完成赋值操作。

//赋值重载
Vector<T>& operator=(Vector<T> v)
{swap(v);return *this;
}

析构函数 

//析构
~Vector()
{if (_start)//避免多次释放{delete[] _start;_start = _finish = _end_of_storage = nullptr;}
}

下标引用operator[ ]

        下标引用重载可以让我们像访问数组元素一样访问容器内容。

//下标引用
T& operator[](size_t i)
{assert(i < size());//防止越界return _start[i];
}
const T& operator[](size_t i) const
{assert(i < size());return _start[i];
}

容量接口

        使用指针减指针的方式来实现容量接口size和capacity功能。

//容量接口
size_t size() const
{return _finish - _start;
}
size_t capacity() const
{return _end_of_storage - _start;
}

迭代器接口

        由于string和vector底层都是顺序存储,所以它们的迭代器相对简单,都是直接使用指针实现。之后我们学习list(链表)时,就会接触到更加复杂的迭代器实现。

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

判空

        当start与finish指向同一处时,容器即为空。 

//判空
bool empty() const
{return _start == _finish;
}

resize

        如果参数n的值小于当前size,则该函数会将size调整为n值,并且删除超出的元素。

        如果参数n的值大于当前size,则会在末尾插入元素至size等于n值。

//调整大小
void resize(size_t n, const T& val = T())
{if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;_finish++;}}
}

reserve

        reserve的实现原理与string相似,当参数n值大于当前容量时,我们才会进行增容操作。这里需要注意:由于容量大小由三个迭代器控制,所以我们重新开辟空间之前需要记录原来的size,便于确定增容之后三个迭代器指向的位置

//增容
void reserve(size_t n)
{if (n > capacity()){size_t old_size = size();//记录原来的sizeT* tmp = new T[n];if (_start)//如果start是空,说明数组为空,不拷贝数据{for (size_t i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

插入和删除

insert和push_back

        insert和push_back负责插入操作。这里需要注意:insert实现时的迭代器失效问题。 

什么是迭代器失效呢?简单的讲,由于vector是使用三个迭代器来维护的,那么如果它们指向的空间被释放,那么就会出现野指针的情况,这三个迭代器的相关操作就是无效的,这就是迭代器失效。迭代器失效的本质就是你管理的内存已经不属于你了

insert出现迭代器失效的原因:很简单,我们在插入数据时,如果空间已满就会增容,此时分配新的空间,然后把内容拷贝过去并释放旧空间。原本我们传入的参数pos(指定的插入位置)是一个指向旧空间的迭代器旧空间被释放后,该迭代器就会失效,从而无法插入数据

解决办法 记录迭代器pos与start的相对距离,当增容完成之后,根据相对距离更新pos即可

        为什么string在进行插入的时候不会发生迭代器失效呢?因为参数pos本身是一个整形,代表要插入的下标,只要该下标不越界,就不会出现失效的情况。当然,这只是插入时不会,并不是一定不会。如果你在外部定义了一个迭代器,当字符串空间被释放后,该迭代器也会失效。此时我们对迭代器重新赋值即可。

接下来我们实现insert和push_back的代码:

iterator insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= finish);//确保插入位置合法if (_finish == _end_of_storage)//空间已满,增容{size_t len = pos - _start;//记录相对位置reserve(capcaity() == 0 ? 4 : 2 * capacity());//增容pos = _start + len;//更新pos}iterator i = _finish - 1;while (i >= pos)//pos之后元素全体向后移动一位{*(i + 1) = *i;i--;}*pos = x;_finish++;return pos;//返回指向新元素的迭代器
}
void push_back(const T& x)
{insert(_finish, x);//直接调用insert
}
erase和pop_back

        erase和pop_back负责删除操作。与insert相同,erase也会发生迭代器失效的问题。但是erase不会造成空间容量的改变,理论上是不会使迭代器失效的。不过,如果有一个迭代器指向末尾元素,删除数据之后,finish前移,该迭代器指向的数据就是非法的,就会造成迭代器失效。所以删除vector的任意元素后,vs编译器就会认为指向该元素的迭代器失效,无法继续使用

        对于这种问题的解决方法也很简单:如果我们只是删除一次数据,迭代器失效也没关系;如果要连续删除,则需要更新迭代器。对此,我们在erase函数中删除元素后,将指向该元素位置的迭代器作为返回值,函数外部需要使用该返回值更新迭代器

erase和pop_back代码实现:

iterator erase(iterator pos)
{assert(pos >= _start && pos < _finish);//确保删除位置合法auto i = pos + 1;while (i < _finish)//pos之后元素全体前移一位{*(i - 1) = *i;i--;}_finish--;return pos;//返回删除位置迭代器
}
void pop_back()
{assert(!empty());//防止空删_finish--;
}

3. 程序全部代码

模拟实现vector的全部代码如下:

#include <iostream>
#include <algorithm>
#include <cassert>
using namespace std;template<class T>
class Vector
{
public://vector的迭代器是一个原生指针typedef T* iterator;typedef const T* const_iterator;//迭代器接口iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}//无参构造Vector(){}//初始化器构造Vector(initializer_list<T> l){reserve(l.size());for (auto& e : l){push_back(e);}}//迭代器区间构造template<class InputIterator>Vector(InputIterator first, InputIterator last){while (first != last){push_back(*first);first++;}}//n个val值构造Vector(size_t n, const T& val = T()){reverse(n);for (size_t i = 0; i < n; i++){push_back(val);}}//拷贝构造Vector(const Vector<T>& v){reverse(v.capacity());for (auto& e : v){push_back(e);}}//赋值重载Vector<T>& operator=(Vector<T> v){swap(v);return *this;}//析构~Vector(){if (_start)//避免多次释放{delete[] _start;_start = _finish = _end_of_storage = nullptr;}}//下标引用T& operator[](size_t i){assert(i < size());//防止越界return _start[i];}const T& operator[](size_t i) const{assert(i < size());return _start[i];}//容量接口size_t size() const{return _finish - _start;}size_t capacity() const{return _end_of_storage - _start;}//判空bool empty() const{return _start == _finish;}//交换void swap(Vector<T>& tmp){std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_end_of_storage, tmp._end_of_storage);}//调整大小void resize(size_t n, const T& val = T()){if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;_finish++;}}}//增容void reserve(size_t n){if (n > capacity()){size_t old_size = size();//记录原来的sizeT* tmp = new T[n];if (_start)//如果start是空,说明数组为空,不拷贝数据{for (size_t i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}}//插入和删除void push_back(const T& x){insert(_finish, x);//直接调用insert}void pop_back(){assert(!empty());//防止空删_finish--;}iterator insert(iterator pos, const T& x){assert(pos >= _start && pos <= finish);//确保插入位置合法if (_finish == _end_of_storage)//空间已满,增容{size_t len = pos - _start;//记录相对位置reserve(capcaity() == 0 ? 4 : 2 * capacity());//增容pos = _start + len;//更新pos}iterator i = _finish - 1;while (i >= pos)//pos之后元素全体向后移动一位{*(i + 1) = *i;i--;}*pos = x;_finish++;return pos;//返回指向新元素的迭代器}iterator erase(iterator pos){assert(pos >= _start && pos < _finish);//确保删除位置合法auto i = pos + 1;while (i < _finish)//pos之后元素全体前移一位{*(i - 1) = *i;i--;}_finish--;return pos;//返回删除位置迭代器}
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;
};

总结

        本篇文章,我们深入了解了vector的底层原理,并成功地模拟实现了它。与传统的动态顺序表不同,它采用了三个迭代器来维护数组,提高了程序灵活性。也正因如此,它的实现难度也有所增大,对于细节把控的要求也很高。之后博主会带领大家学习一个新容器--list。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

指针的运用

接下来我将会用的话&#xff0c;讲解我对指针运用仅有的印象 1.解引用 int a23; int*p&a; *p666; 而*p666&#xff1b;&#xff0c;便是解引用操作&#xff0c;跟简单地说*p便是解引用&#xff0c;它的意思是&#xff0c;对p中所储存的地址所在位置的内容进行操作&#xf…

三周精通FastAPI:38 针对不同的编程语言来生成客户端

官方文档&#xff1a;https://fastapi.tiangolo.com/zh/advanced/generate-clients/ 生成客户端 因为 FastAPI 是基于OpenAPI规范的&#xff0c;自然您可以使用许多相匹配的工具&#xff0c;包括自动生成API文档 (由 Swagger UI 提供)。 一个不太明显而又特别的优势是&#…

广告联盟有哪些

随着互联网的发展&#xff0c;越来越多的人开始投身于网站建设和运营。对于站长来说&#xff0c;如何在提供优质内容的同时获取收益是一个重要的问题。广告联盟作为一种常见的盈利模式&#xff0c;受到了广大站长的青睐。本文将介绍5个适合国内站长的广告联盟平台&#xff0c;帮…

兵马未动,粮草先行-InnoDB统计数据是如何收集的

我们前面介绍查询成本的时候经常用到一些统计数据&#xff0c;比如通过SHOW TABLE STATUS可以看到关于表的统计数据&#xff0c;通过SHOW INDEX可以看到关于索引的统计数据&#xff0c;那么这些统计数据是怎么来的呢&#xff1f;它们是以什么方式收集的呢&#xff1f;本章将聚焦…

【Promise】JS 异步之宏队列与微队列

文章目录 1 原理图2 说明3 相关面试题3.1 面试题13.2 面试题23.3 面试题33.4 面试题4 1 原理图 2 说明 JS 中用来存储待执行回调函数的队列包含 2 个不同特定的队列&#xff1a;宏队列和微队列。宏队列&#xff1a;用来保存待执行的宏任务(回调)&#xff0c;比如&#xff1a;定…

基础概念理解

一&#xff0c;数据结构分类 连续结构&#xff0c;跳转结构。 二&#xff0c;对变量的理解 在 C 语言中&#xff0c;变量是用于存储数据的抽象符号。变量本质上是一块内存区域的标识符&#xff08;即它代表内存中的某一块区域&#xff09;&#xff0c;用来存储数据&#xff…

C 学习(4)

return 0; 前提&#xff1a;C 语言规定&#xff0c;main()是程序的入口函数&#xff0c;即所有的程序一定要包含一个main()函数。程序总是从这个函数开始执行&#xff0c;如果没有该函数&#xff0c;程序就无法启动。其他函数都是通过它引入程序的。 main()的写法&#xff0c…

欺诈文本分类检测(十八):基于llama.cpp+CPU推理

1. 前言 前文我们用Lora训练出自己的个性化模型后&#xff0c;首先面临的问题是&#xff1a;如何让模型在普通机器上跑起来&#xff1f;毕竟模型微调时都是在几十G的专用GPU上训练的&#xff0c;如果换到只有CPU的普通电脑上&#xff0c;可能会面临几秒蹦一个词的尴尬问题。 …

工程数学线性代数(同济第七版)附册课后习题答案PDF

《线性代数附册 学习辅导与习题全解》是与同济大学数学科学学院编《工程数学 线性代数》第七版教材配套的教学辅导书&#xff0c;由同济大学作者团队根据教材内容和要求编写而成。本书在《工程数学 线性代数》第六版附册&#xff08;即辅导书&#xff09;的基础上修改而成。全书…

传输层协议、ACL

第六章 传输层协议、ACL 文章目录 第六章 传输层协议、ACL1.TCP和UDP协议1.1 TCP协议1.2 TCP报文段1.3 TCP连接 2.UDP协议3.ACL概述ACL原理及种类ACL组成规则编号通配符&#xff08;反掩码&#xff09; 4.ACL应用ACL匹配规则ACL匹配规则 1.TCP和UDP协议 TCP/IP协议族的传输层协…

(蓝桥杯C/C++)——搜索

一、回溯法 1.回溯法简介 回溯法一般使用 ** DFS(深度优先搜索) ** 实现&#xff0c;DFS是一种遍历或搜索图、树或图像等数据结构的算法&#xff0c;当然这个图、树未必要存储下来(隐式处理就是回溯法)&#xff0c;常见的是通过某种关系构造出的搜索树&#xff0c;搜索树一般…

Turtlebot3 buger 硬件与操作平台详细介绍

引言 TurtleBot3 有三个版本&#xff0c;分别是紧凑型的 Burger、功能更强的 Waffle和性能提升的 Waffle Pi&#xff0c;分别适用于不同的应用需求。使用 Raspberry Pi 作为主控单板计算机&#xff08;SBC&#xff09;&#xff0c;而 Waffle Pi 可以使用更强大的 NVIDIA Jetson…

Ubuntu实现双击图标运行自己的应用软件

我们知道在Ubuntu上编写程序&#xff0c;最后编译得到的是一个可执行文件&#xff0c;大致如下 然后要运行的时候在终端里输入./hello即可 但是这样的话感觉很丑很不方便&#xff0c;下边描述一种可以类似Windows上那种双击运行的实现方式。 我们知道Ubuntu是有一些自带的程序…

Chromium Mojo(IPC)进程通信演示 c++(4)

122版本自带的mojom通信例子仅供学习参考&#xff1a; codelabs\mojo_examples\01-multi-process 其余定义参考文章&#xff1a; Chromium Mojo(IPC)进程通信演示 c&#xff08;2&#xff09;-CSDN博客 01-mojo-browser.exe 与 01mojo-renderer.exe进程通信完整例子。 一、…

基于Prometheus的client_golang库实现应用的自定义可观测监控

文章目录 1. 安装client_golang库2. 编写可观测监控代码3. 运行效果4. jar、graalvm、golang编译运行版本对比 前文使用javagraalvm实现原生应用可观测监控&#xff1a; prometheus client_java实现进程的CPU、内存、IO、流量的可观测&#xff0c;但是部分java依赖包使用了复杂…

【C++课程学习】:继承(上)(详细讲解)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 一.继承的概念和定义 &#x1f384;继承的概念&#xff1a; &#x1f384;继承的定义&#xff1a; …

PVE纵览-备份与快照指南

PVE纵览-备份与快照指南 文章目录 PVE纵览-备份与快照指南摘要1 备份与快照概述定义与区别备份与快照在PVE中的应用场景 2 PVE 备份功能详解备份类型与策略配置备份任务自动化备份管理 3 PVE 快照功能详解快照的工作原理快照的创建与恢复机制快照对系统性能的影响快照的使用场景…

Android JNI 技术入门指南

引言 在Android开发中&#xff0c;Java是一种主要的编程语言&#xff0c;然而&#xff0c;对于一些性能要求较高的场景&#xff08;如音视频处理、图像处理、计算密集型任务等&#xff09;&#xff0c;我们可能需要使用到C或C等语言来编写底层的高效代码。为了实现Java代码与C…

供应商srm管理,招投标管理,电子采购管理,在线询价,在线报价,供应商准入审核(java代码)

前言&#xff1a; 随着互联网和数字技术的不断发展&#xff0c;企业采购管理逐渐走向数字化和智能化。数字化采购平台作为企业采购管理的新模式&#xff0c;能够提高采购效率、降低采购成本、优化供应商合作效率&#xff0c;已成为企业实现效益提升的关键手段。系统获取在文末…

Java 函数接口Supplier【供给型接口】简介与示例

Java中四个重要的函数式接口&#xff1a;Function、Predicate、Consumer和Supplier。这些接口是函数式编程的基础&#xff0c;Function用于转换操作&#xff0c;Predicate用于进行条件判断&#xff0c;Consumer用于消费输入而不产生输出&#xff0c;而Supplier则用于提供值但不…