【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,一经查实,立即删除!

相关文章

Java面试之Java基础常见面试题

1、 Java中的基本数据类型有哪些&#xff1f; 1、整数类型&#xff1a;byte&#xff08;1字节&#xff09;、short&#xff08;2字节&#xff09;、int&#xff08;4字节&#xff09;、long&#xff08;8字节&#xff09; 2、浮点类型&#xff1a;float&#xff08;4字节&…

基于STM32的水族馆鱼缸系统设计172

基于STM32的水族馆鱼缸系统设计(局域网)(172) 文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】硬件模块组成【3】系统功能模块划分【4】ESP8266模块配置【5】Qt上位机网络连接原理(Android开发)【6】自动换水原理1.2 项目开发背景1.3 开发工具的选择1.4 系统框架图1.5 …

AEC10 SA计算整理 --- flash部分

整理了AE计算的一些参数和计算公式&#xff0c;方便查找。 PreflashSafeAggSA PreflashSafeSALuma FrameLumaBE16x16 PreflashSafeSATarget [set param/tr: lux]50.0 PreflashSafeSAAdjRatioNoScale PreflashSafeSATarget / PreflashSafeSALuma PreflashSafeSAAdjRatioCapNoS…

ExDark数据集标签转yolo格式(易懂)

ExDark数据集标签转yolo格式&#xff08;易懂&#xff09; 一、前言 知道大家为何而来&#xff0c;因此作者不必废话直接上代码。代码已经详细注释&#xff0c;如果读者还有不懂的地方&#xff0c;在评论区留言&#xff0c;我将在48小时内回复你&#xff08;如果我没毕业的话…

接口综合管理站iGR-IMS的产品特点

接口综合管理站iGR-IMS的产品特点可以归纳为以下几点&#xff1a; 1. 高安全性&#xff1a;产品选用Linux作为软件基础平台&#xff0c;同时能够配合电力专用隔离装置&#xff0c;实现跨安全区的单向数据传输&#xff0c;保证了数据传输的安全性。 2. 高性能&#xff1a;产品…

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

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

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

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

canal+mq将数据同步到redis中的一些类型转换问题

在将 Canal 捕获到的数据库变更同步到 RabbitMQ 时&#xff0c;通常需要将变更事件的数据从 Java 对象转换为一种通用的数据格式&#xff0c;如 JSON。这样可以确保数据在不同系统之间传递时的兼容性。以下是将 Canal 数据同步到 RabbitMQ 并进行数据类型转换的示例代码。 1. …

Cherno 游戏引擎笔记记录(33~45)

好久不见。 My Github REPO(GitHub - JJJJJJJustin/Nut: The game_engine which learned from Cherno) 源码笔记&#xff0c;希望帮到你 :-} -------------------相机&原理---------- 》》》》查看这两篇说明&#xff0c;一个是坐标系统&#xff0c;一个是摄像机 &#xf…

通过Spring Boot结合实时流媒体技术对考试过程进行实时监控

本章将深入探讨考试系统中常见的复杂技术问题&#xff0c;并提供基于Spring Boot 3.x的解决方案。涵盖屏幕切换检测与防护、接打电话识别处理、行为监控摄像头使用、网络不稳定应对等&#xff0c;每篇文章详细剖析问题并提供实际案例与代码示例&#xff0c;帮助开发者应对挑战&…

大语言模型系列-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; …

Java中的AOP编程详解

Java中的AOP编程详解 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 1. 什么是AOP&#xff1f; AOP&#xff08;Aspect-Oriented Programming&#xff0c;面…

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;玩家的目标通常是找到出口或…

算法力扣刷题——总结篇【四】和string类详解

前言 字符串章节部分跟随学习结束&#xff0c;作出总结。 一、题目及方法总结 &#xff08;1&#xff09;反转字符串&#xff1a;双指针法。 反转全部字符串&#xff0c;i在开头&#xff0c;j在结尾&#xff1b;判断条件i < j ;每隔2k反转前k个字符&#xff0c;i改成i 2…

PermissionError: [Errno 13] Permission denied: ‘/tmp/gradio...‘

无管理员权限修改 Gradio 默认路径遇到的 PermissionError 问题 在使用 Gradio 进行开发和部署时&#xff0c;可能会遇到如下报错&#xff1a; PermissionError: [Errno 13] Permission denied: /tmp/gradio/tmpzo5r9g_k.png报错分析 上述报错是由于在没有权限访问指定路径时…

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

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

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

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