销售网站设计方案/百度seo指数查询
🌟🌟作者主页: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。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤
相关文章:

【c++丨STL】vector模拟实现
🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:C、STL 目录 前言 一、vector底层刨析 二、模拟实现 1. 属性、迭代器以及函数声明 2. 功能实现 交换两个容器的内容 构造函数 拷贝构造 赋值重载 析构…...

SQLAlchemy 介绍与实践
postgresql 实践 pydantic 实践 1. SQLAlchemy 介绍 SQLAlchemy 是一个 ORM 框架。SQLAlchemy 是一个用于 Python 的 SQL 工具和对象关系映射(ORM)库。它允许你通过 Python 代码来与关系型数据库交互,而不必直接编写SQL语句。 简单介绍一下…...

docker进行SRS直播服务器搭建
docker进行SRS直播服务器搭建 docker构建参考地址: 地址: https://github.com/ossrs/srs https://ossrs.net/lts/zh-cn/docs/v5/doc/getting-started docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 \-p 8000:8000/udp -p 10080:10080/udp ossrs/sr…...

windows server2019下载docker拉取redis等镜像并运行项目
一、基本概念 1、windows server 指由微软公司开发的“Windows”系列中的“服务器”版本。这意味着它是基于Windows操作系统的,但专门设计用于服务器环境,而不是普通的桌面或个人用户使用。主要用途包括服务器功能、用户和资源管理、虚拟化等 2、dock…...

数据结构(8.7_2)——败者树
多路平衡归并带来的问题 什么是败者树 败者树的构造 败者树的使用 败者树在多路平衡归并中的应用 败者树的实现思路 总结...

设计模式-七个基本原则之一-里氏替换原则
里氏替换原则(LSP)面向对象六个基本原则之一 子类与父类的替代性:子类应当能够替代父类出现的任何地方,且表现出相同的行为。行为的一致性:子类的行为必须与父类保持一致,包括输入和输出、异常处理等。接口…...

k8s中基于overlay网络和underlay网络的网络插件分别有哪些
在 Kubernetes 中,不同的网络插件会使用 overlay 或 underlay 网络来连接 Pod 和节点。以下是基于 overlay 网络和 underlay 网络的常见 Kubernetes 网络插件: 1. 基于 Overlay 网络的插件 这些插件通过隧道封装技术(如 VXLAN、GRE 等&#…...

一文详解java的数据类型
1. 题记 Java是一门对数据类型敏感的语言,本博文主要总结介绍java语言的数据类型。 2. java的数据类型 Java 的数据类型分为基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)。 2.1 基本数…...

Flink API 的层次结构
Apache Flink 提供了多层 API,每层 API 针对不同的抽象层次和用途,使得开发者可以根据具体需求选择合适的 API 层次。以下是 Flink API 的层次结构及其简要说明:...

lua入门教程:math
在Lua中,math库是一个非常重要的内置库,它提供了许多用于数学计算的函数。这些函数可以处理各种数学运算,包括基本的算术运算、三角函数、对数函数、随机数生成等。结合你之前提到的Lua中的数字遵循IEEE 754双精度浮点标准,我们可…...

ROS2简介与Ubuntu24.04中安装指南
之前安装了一个版本,但是不愿意写blog,现在想想自己就是个沙子立个flag,每次配置项目,写流程blog ROS简介 ROS(Robot Operating System)是一个开源的机器人软件平台,提供了许多工具和库来帮助…...

命令行工具PowerShell使用体验
命令行工具PowerShell使用 PowerShell是微软开发的一种面向对象的命令行Shell和脚本语言环境,它允许用户通过命令行的方式管理操作系统。相较于传统CMD,PowerShell增加了面向对象的程序设计框架,拥有更强大的功能和扩展性。使用PowerShell可…...

MongoDB 详解:深入理解与探索
在当今的数据库领域,MongoDB 以其独特的特性和强大的功能,成为了众多开发者和企业的首选。本文将对 MongoDB 进行详细的介绍,包括其特点、应用场景、流程图以及源码分析。 一、MongoDB 概述 MongoDB 是一个基于分布式文件存储的开源数据库系…...

使用 Elasticsearch 构建食谱搜索(一)
作者:来自 Elastic Andre Luiz 了解如何使用 Elasticsearch 构建基于语义搜索的食谱搜索。 简介 许多电子商务网站都希望增强其食谱搜索体验。正确使用语义搜索可以让客户根据更自然的查询(例如 “something for Valentines Day - 情人节的礼物” 或 “…...

sealos部署K8s,安装docker时master节点突然NotReady
1、集群正常运行中,在集群master-1上安装了dockerharbor,却发现master-1节点NotReady,使用的网络插件为 Cilium #安装docker和harbor(docker运行正常) rootmaster-1:/etc/apt# apt install docker-ce5:19.03.15~3-0~u…...

使用vite+react+ts+Ant Design开发后台管理项目(五)
前言 本文将引导开发者从零基础开始,运用vite、react、react-router、react-redux、Ant Design、less、tailwindcss、axios等前沿技术栈,构建一个高效、响应式的后台管理系统。通过详细的步骤和实践指导,文章旨在为开发者揭示如何利用这些技术…...

Spring Boot实现多数据源连接和切换
文章目录 前言一、多数据源配置与切换方案二、实现步骤1. 创建多个 DataSource 配置类2. 创建 DataSource 配置类3. 创建动态数据源路由类4. 实现 DynamicDataSource 类5. 创建 DataSourceContextHolder 来存储当前的数据源标识6. AOP 方式切换数据源7. 自定义注解来指定数据源…...

发布 VectorTraits v3.0(支持 X86架构的Avx512系列指令集,支持 Wasm架构及PackedSimd指令集等)
文章目录 支持 X86架构的Avx512系列指令集支持Avx512时的输出信息 支持 Wasm架构及PackedSimd指令集支持PackedSimd时的输出信息VectorTraits.Benchmarks.Wasm 使用说明 新增了向量方法支持 .NET 8.0 新增的向量方法提供交织与解交织的向量方法YGroup3Unzip的范例代码 提供重新…...

详解如何创建SpringBoot项目
目录 点击New Project 选择依赖 简单使用SpringBoot 前面已经讲解了如何获取IDEA专业版,下面将以此为基础来讲解如何创建SpringBoot项目。 点击New Project 选择依赖 注意,在选择SpringBoot版本时,不要选择带SNAPSHOT的版本。 这样&#…...

IT架构管理
目录 总则 IT架构管理目的 明确组织与职责 IT架构管理旨在桥接技术实施与业务需求之间的鸿沟,通过深入理解业务战略和技术能力,推动技术创新以支持业务增长,实现技术投资的最大价值。 设定目标与范围 IT架构管理的首要目的是确立清晰的组织…...

Feign入门实践
引言 随着微服务架构的兴起,服务间的通信变得越来越频繁和复杂。为了简化服务之间的调用过程,提高开发效率和系统的可维护性,Spring Cloud 生态系统提供了多种解决方案,其中 OpenFeign 是一种声明式的 HTTP 客户端,它使…...

Leetcode 买卖股票的最佳时机 Ⅱ
使用贪心算法来解决此问题,通过在价格上涨的每一天买入并在第二天卖出的方式,累计所有上涨的利润,以实现最大收益。关键点是从第二天开始遍历,并且只要当前比前一天价格高,我们就在前一天买入然后第二天卖出去。下面是…...

书生大模型实战营-玩转HF/魔搭社区闯关任务
通过Github Codespace下载InternLM模型并运行 本篇博客是记录《书生大模型实战营第四期-玩转HF/魔搭/魔乐》章节的闯关任务从HF上下载模型文件,对实战营感兴趣的小伙伴也可以扫码报名哦。 一、通过模版创建Codespace环境 访问codespace 点击Jupyter Notebook 模版…...

混响(Reverb):原理、应用与发展趋势的深度解析
目录 引言1. 混响的基本原理2. 混响的应用3. 混响的技术实现4. 混响的未来发展趋势5. 总结 引言 混响(Reverb)是音频信号处理中的重要概念之一,在自然界和音频工程中都扮演着关键角色。从音乐制作到语音识别,从电影音效到虚拟现实…...

Java学习教程,从入门到精通,Java修饰符语法知识点及案例代码(23)
1.Java修饰符语法知识点及案例代码 Java修饰符用于改变类、方法、变量、接口等元素的行为和可见性。主要分为两大类:访问修饰符和非访问修饰符。 访问修饰符(Access Modifiers) public 提供最大的访问权限,任何类都可以访问。使…...

钉钉小程序使用getApp实现类型provide inject的功能 应用场景:解决页面同步子组件弹窗的滚动问题
前言:在开发钉钉小程序的时候 组件内部的弹窗滚动会带着视图同步滚动 所以需要在组件内部弹窗显示的时候禁用视图的scroll滚动 由于我组件封装的比较深 不可能逐级传递 dd也么有provide的语法 所以我使用的getApp 完成控制的效果 最终完美运行 觉得有帮助相互关注一下 后续会持…...

标准化 Git 提交信息的约定
在使用 Git 进行版本控制时,良好的提交信息可以帮助团队成员更好地理解每次提交的目的和影响。为了规范化提交信息,一些团队采用了特定的格式或约定,比如 Angular 团队提出的 Commit Message Conventions。这种规范有助于自动化工具的使用&am…...

React教程(详细版)
React教程(详细版) 1,简介 1.1 概念 react是一个渲染html界面的一个js库,类似于vue,但是更加灵活,写法也比较像原生js,之前我们写出一个完成的是分为html,js,css&…...

Perfect Forwarding(完美转发)
文章目录 1. 引用折叠2. 万能引用3. 完美转发3.1对比:std::move and std::forward比较 3.2使用时机3.3 返回值优化(RVO)两个前提条件注意事项 4. 完美转发失败情况完美转发失败五种情况 完美转发的实现要依赖于模版类型推导和引用折叠和万能引用。 1. 引…...

PHP露营地管理平台小程序系统源码
⛺️【露营新风尚】露营地管理平台系统全攻略⛺️ 🏕️一、露营热潮下的管理难题:如何高效运营露营地?🤔 随着露营文化的兴起,越来越多的人选择在大自然中享受宁静与自由。然而,露营地的管理却面临着诸多…...