【C++】vector的底层原理及实现

文章目录

  • vector的底层结构
  • 迭代器
  • 容量操作
    • size()
    • capacity()
    • reserve()
    • resize()
  • 默认成员函数
    • 构造
      • 无参构造函数
      • 带参构造函数
    • 析构
    • 拷贝构造
    • 赋值重载
  • operator[ ]
  • 插入删除操作
    • insert()任意位置插入
    • erase()任意位置删除
    • push_back()尾插
    • pop_back()尾删

vector的底层结构

我们的目的不是去模拟实现vector,而是更深入地理解vector的底层原理,更好地提升自己。本篇将简单地模拟实现vector,更好地理解它的构造和原理。参考:vector使用说明

在C++的STL中,vector是最常用的容器之一,底层是一段连续的线性内存空间(泛型的动态类型顺序表),可以支持随机访问。vector可以存储各种类型,int、char、string等,所以它是一种类模板,可以套用各种类型。

STL标准库中vector的核心是这样定义的,这里的alloc涉及到内存池的知识,我们可以先不用管。
在这里插入图片描述
vector的底层会用三个指针,利用三个指针相减来实现动态存储。

我们自己定义一个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的迭代器是一个原生指针,他的迭代器和string相同都是操作指针来遍历数据。

迭代器返回的是存储数据的起始位置和末尾的下一个位置,区间是左开右闭的[_start, _finish)
实现了迭代器,我们在代码测试时就可以使用范围for了。

iterator begin()
{return _start;
}
iterator end()
{return _finish;
}
const_iterator begin() const
{return _start;
}
const_iterator end() const
{return _finish;
}

容量操作

size()

size_t size() const
{return _finish - _start;
}

capacity()

size_t capacity() const
{return _end_of_storage - _start;
}

reserve()

这个函数十分重要,因为vector许多地方都会用reserve()去扩容,并且还有几个非常容易搞错的地方。

reserve只能扩容,不能增容,因此要进行判断是否需要扩容,缩容就不进行操作。

扩容的步骤是:申请一块更大的新空间,再将旧空间数据移动到新空间中,最后释放旧空间。

下面这种写法有什么问题?

void reserve(size_t n)
{if (n > capacity()){//申请新空间T* tmp = new T[n];if (_start){memcpy(tmp, _start, sizeof(T) * old_size);//释放旧空间delete[] _start;}_start = tmp;_finish = tmp + size();_end_of_storage = _start + n;}
}

错误一:倒数第二行的_finish没有发生变化

看这段代码的最后三行,_start先指向新空间的起始位置,_finish再调用size()的话,此刻的size()已经不是当初的size()了。size()的返回值是_finish - _start,而原本的_start已经改变成了tmp,此时_finish的值 = _start + _finish - _start = _finish;所以_finish没有发生变化。


解决方法有两种:
1._start和_finish赋值的顺序调换一下,先改变_finish,再改变_start。
2.挪动数据前先保留原本的size();

我们这里采用第二种写法。

void reserve(size_t n)
{if (n > capacity()){T* tmp = new T[n];size_t old_size = size();//保留之前的sizeif (_start){memcpy(tmp, _start, sizeof(T) * old_size);delete[] _start;}_start = tmp;_finish = tmp + old_size;_end_of_storage = _start + n;}
}

2.不能用memcpy去拷贝内容,而用赋值去拷贝内容。

对于int、char等内置类型,可以使用memcpy不会出问题。对于自定义类型一般也没有问题,但是对于动态申请资源的自定义类型,memcpy就会发生浅拷贝,导致一块空间析构两次。

比如vector<string>类型,此时的T是string类型。上一篇我们了解过string的底层原理,string底层用的是char*指针_str来存储字符串的地址,因此需要动态申请空间。

所以我们是在申请的空间(_start)上面又申请了一块空间(_str),如果我们使用memcpy去拷贝_start中的内容到tmp中;就会把申请的_str指向的地址拷贝给tmp,这样_start和tmp中的_str就会指向同一块空间,再执行delete[] _start;的时候就会执行string的析构函数把这块空间释放。
在这里插入图片描述
所以不能用memcpy浅拷贝,解决方法:
一个个遍历用=赋值,对于string这种深拷贝的类,调用的是string的赋值重载完成深拷贝。
注意:这里的string使用的是STL库中的string类。

void reserve(size_t n)
{if (n > capacity()){T* tmp = new T[n];size_t old_size = size();//保留之前的sizeif (_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 = _start + n;}
}

resize()

resize函数用于改变vector的大小,调整vector中元素的数量。

n > size():多余空间添加元素(第二个参数)
n <= 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;}}
}

默认成员函数

构造

无参构造函数

vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
{}

使用

vector<int> v1;
vector<string> s2;

带参构造函数

vector(size_t n, const T& val = T())
{resize(n, val);
}

使用

vector<int> v1(10);//开辟5个int大小空间,默认初始化为0
vector<int> v2(10, 1);//开辟10个int大小空间,并初始化为1
vector<string> s2(10,"abc");//开辟10个string大小空间,并初始化为"abc"

析构

~vector()
{if (_start){delete[] _start;_start = _finish = _end_of_storage = nullptr;}
}

拷贝构造

写法一:尾插法

vector(const vector<T>& v): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{reserve(v.size());for (auto& e : v)//引用防止调用拷贝构造{push_back(e);}
}

写法二:常规法

vector(const vector<T>& v): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{_start = new T[v.size()];for (size_t i = 0; i < v.size(); i++){_start[i] = v._start[i];}_finish = _start + v.size();_end_of_storage = _start + v.capacity();
}

赋值重载

和string一样,我们直接复用标准库的swap函数。

注意:用swap的话,拷贝构造的参数要用传值传递(不能改变实参),且不能用const修饰(发生交换)

void swap(vector<T>& v)
{std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}

operator[ ]

两个重载版本,一个普通对象调用,一个const对象调用。

T& operator[](size_t pos)
{assert(pos < size());return _start[pos];
}
const T& operator[](size_t pos) const
{assert(pos < size());return _start[pos];
}

插入删除操作

insert()任意位置插入

插入之后,如果原始空间发生扩容,则pos指针仍指向原来空间的位置,迭代器就会发生失效。所以我们需要在扩容之前保存pos的相对位置,然后重新指向正确位置。

//insert后 迭代器可能会失效,扩容就会引起失效
iterator insert(iterator pos, const T& val)
{assert(pos >= _start && pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;//保存pos相对位置,防止失效size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);//解决迭代器失效问题pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;end--;}*pos = val;_finish++;return pos;
}

但是,这样只保证了能正确插入数据。如果在使用时,我们insert()一个元素后,迭代器还会可能失效,因为我们使用的传值传递,形参不会影响实参。因此insert()函数返回pos这个位置的迭代器,我们在外部使用的时候更新迭代器即可。

vector<int> v(5, 1);
vector<int>::iterator p = v.begin() + 1;
p = v.insert(p, 10);//更新迭代器

erase()任意位置删除

iterator erase(iterator pos)
{assert(pos >= _start && pos < _finish);iterator end = pos;while (end < _finish - 1) {*end = *(end + 1);end++;}_finish--;return pos;
}

删除虽然不会扩容(不用保存pos指针的相对位置),但是删除一个元素后,后面的元素都会向前移动,此时迭代器指向空间虽然不变,但内容变成了下一个元素,我们在使用的时候要注意这个问题。

int main()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(2);v1.push_back(6);auto it = v1.begin();//法一:边使用边更新while (it != v1.end()){if (*it % 2 == 0){it = v1.erase(it);}else{it++;}}//法二:使用完将迭代器减一,防止++出现走两步的情况while (it != v1.end()){if (*it % 2 == 0){v1.erase(it);it--;}it++;}
}

push_back()尾插

实现insert()函数后,我们就可以直接复用。

void push_back(const T& val)
{/*if (_finish == _end_of_storage){size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);}*_finish++ = val;*/insert(end(), val);
}

pop_back()尾删

同样直接复用erase(),让erase()自己判断合法性。

void pop_back()
{/*assert(size() > 0);_finish--;*/erase(_finish - 1);
}

vector模拟实现代码

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

开源之夏|祝贺MatrixOne开源社区项目中选同学!

在本届「开源之夏 2024」活动中&#xff0c;MatrixOne开源社区共计上线3个项目任务&#xff0c;最终有 3位同学成功突围。接下来让我们看看每个项目的详细中选情况&#xff1a; 中选学生公示 项目名称&#xff1a;基于大语言模型的操作系统任务自动识别&#xff0c;拆解&#…

Stable Diffusion教程:如何实现人脸一致

在AI绘画中&#xff0c;一直都有一个比较困难的问题&#xff0c;就是如何保证每次出图都是同一个人。今天就这个问题分享一些个人实践&#xff0c;大家和我一起来看看吧。 一. 有哪些实现方式 方式1&#xff1a;固定Seed种子值。 固定Seed种子值出来的图片人物确实可以做到一…

大语言模型系列-Transformer(二)

Transformer 模型的入门可以从以下几个方面开始&#xff1a; 1. 理解基本概念 序列到序列&#xff08;Sequence-to-Sequence&#xff09;任务&#xff1a;Transformer 模型主要用于这类任务&#xff0c;如机器翻译、文本摘要等。注意力机制&#xff08;Attention Mechanism&a…

PyTorch基础(23)-- Tensor.scatter_()方法

一、前言 本次要介绍的函数为Tensor.scatter_函数&#xff0c;也是PyTorch中常用的函数之一&#xff0c;但遗憾的是&#xff0c;我想在网络上查询该函数的用法时&#xff0c;大部分的文章都是直接给出一个示例&#xff0c;看完之后&#xff0c;其中的原理我还是无法理解&#…

python生成器在读取接口用例中应用解析

Python生成器Generator Python生成器&#xff08;Generator&#xff09;是一种特殊类型的函数&#xff0c;它可以通过yield语句逐步生成值。 生成器提供了一种延迟计算的方式&#xff0c;可以逐步产生结果&#xff0c;而不是一次性生成所有的值。 1、生成器原理&#xff1a; …

2024年【A特种设备相关管理(A4电梯)】试题及解析及A特种设备相关管理(A4电梯)模拟试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 A特种设备相关管理&#xff08;A4电梯&#xff09;试题及解析根据新A特种设备相关管理&#xff08;A4电梯&#xff09;考试大纲要求&#xff0c;安全生产模拟考试一点通将A特种设备相关管理&#xff08;A4电梯&#x…

Mac密室逃脱游戏推荐:Escape Simulator for mac安装包

Escape Simulator 是一款逃生模拟游戏&#xff0c;玩家在游戏中需要寻找线索、解决谜题&#xff0c;以逃离各种房间或环境。这种类型的游戏通常设计有多个关卡或场景&#xff0c;每个场景都有不同的设计和难度。 在 Escape Simulator 中&#xff0c;玩家的目标通常是找到出口或…

东方韵味:红酒与茶道的很好邂逅

在古老的东方&#xff0c;茶道与红酒各自承载着深厚的文化底蕴和历史传承。当这两大传统文化碰撞、交融&#xff0c;仿佛展开了一幅绚烂多姿的画卷&#xff0c;既展现了东方的神秘韵味&#xff0c;又融入了红酒的异国风情。今天&#xff0c;就让我们一同探索这场红酒与茶道的很…

详解微服务应用灰度发布最佳实践

作者&#xff1a;子丑 本次分享是站在 DevOps 视角的灰度发布实践概述&#xff0c;主要内容包括以下四个方面&#xff1a; 第一&#xff0c;灰度发布要解决的问题&#xff1b; 第二&#xff0c;灰度发布的四种典型场景&#xff1b; 第三&#xff0c;如何把灰度发布融入到应…

2024年07月03日 Redis部署方式和持久化

Redis持久化方式&#xff1a;RDB和AOF&#xff0c;和混合式 RDB&#xff1a;周期备份模式&#xff0c;每隔一段时间备份一份快照文件&#xff0c;从主线程Fork一个备份线程出来备份&#xff0c;缺点是会造成数据的丢失。 AOF&#xff1a;日志模式&#xff0c;每条命令都以操作…

Java8环境安装(jdk1.8安装)详细教程

Java 8环境安装&#xff08;jdk1.8安装&#xff09;详细教程 Java 8&#xff08;也称为JDK 1.8&#xff09;&#xff0c;是Oracle公司于2014年3月发布的一个重要的Java语言版本。这个版本自发布以来&#xff0c;因其众多的新特性和改进&#xff0c;被认为是Java语言发展历程中…

itk::ShapedNeighborhoodIterator类C2516问题

错误问题&#xff1a; 1>C:\itk\src-5.3.0\Modules\Core\Common\include\itkShapedNeighborhoodIterator.h(183,1): error C2516: itk::ShapedNeighborhoodIterator<TImage,TBoundaryCondition>::ConstIterator: is not a legal base class 1>C:\itk\src-5.3.0\Mo…

【FFmpeg】avcodec_send_frame函数

目录 1.avcodec_send_frame1.1 将输入的frame存入内部buffer&#xff08;encode_send_frame_internal&#xff09;1.1.1 frame的引用函数&#xff08;av_frame_ref &#xff09;1.1.1.1 帧属性的拷贝&#xff08;frame_copy_props&#xff09;1.1.1.2 buffer的引用函数&#xf…

Appium 使用隐式等待,无法执行代码怎么办?

简介 添加等待是为了确保自动化脚本在执行过程中与应用程序之间的同步和稳定性。 应用程序的响应时间是不确定的&#xff0c;可能存在网络延迟、加载时间、动画效果等因素。如果在执行自动化脚本时没有适当的等待机制&#xff0c;脚本可能会在应用程序还未完成相应操作或加载完…

转让5000万无区域能源公司要求和流程

国家局的公司&#xff0c;也就是无地域无区域性的公司名称。这样的公司是还可以继续注册的&#xff0c;但是想要拥有国家局无区域的名称就不是那么容易的了。总局的企业要求高&#xff0c;也是实力的体现。对字号有保护。所以有很多人都对无地域的名称一直情有独钟。现有一家名…

智能视频监控平台LntonCVS视频监控汇聚平台系统详细介绍

视频监控综合管理平台是专为大中型用户设计的安防管理软件&#xff0c;旨在实现跨区域网络化视频监控的集中管理。该平台集成了信息资源管理、设备管理、用户管理、网络管理和安全管理等多项功能&#xff0c;能够集中管理监控中心的所有视频图像&#xff0c;支持多品牌设备联网…

AI基准测评(下):视频生成、代码能力、逻辑推理,AI是否已经超越人类?

本文作者&#xff1a;王加龙&#xff0c;阿里云高级算法专家 文章推荐 AI实测&#xff5c;基于图像、语言与声音&#xff0c;人工智能是否已经超越了人类&#xff1f; 知乎AI产品“直答”正式上线&#xff01;文心一言4.0 Turbo来袭&#xff0c;可为农民提供专业指导&#x…

Fork/Join框架

什么是 Fork/Join Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架&#xff0c; 是一个把大任务分割成若干个小任务&#xff0c;最终汇总每个小任务结果后得到大任务结果的框架。 分治 我们再通过 Fork 和 Join 这两个单词来理解下 Fork/Join 框架&#xff0c;F…

数据库设计规范(DOC文件)

1 编写目的 2 数据库策略 2.1 数据库对象长度策略 2.2 数据完整性策略 2.3 规范化设计与性能之间的权衡策略 2.4 字段类型的定义与使用策略 3 命名规范 3.1 数据库命名规则 3.2 数据库对象命名的一般原则 3.3 表空间(Tablespace)命名规则 3.4 表(Table)命名规则 3.5…

Java 虚拟机 一

运行时数据区 我们先看线程隔离的数据区 程序计数器 程序计数器&#xff08; Program Counter Register&#xff09; 是一块较小的内存空间&#xff0c; 它可以看作是当前线程所执行的字节码的行号指示器。 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执…