C++【深入底层,手撕vector】

        vector是向量的意思,看了vector的底层实现之后,能够很明确的认识到它其实就是我们经常使用的顺序表。在我们的认知中,顺序表会有一个数组、数据的size以及容量的大小。vector作为一个向量容器,它可以存放任意类型的数据。所以在实现时一定是一个类模板。再来谈它的成员属性,我在这里仿照库函数的实现,定义了三个迭代器:_start、_finish、_end_of_sotrage;

1、基本框架

namespace ltq
{template<class T>class vector{public:typedef T* iterator;vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){}~vector(){delete[] _start;//涉及到后续的空间申请,所以需要释放。_start = nullptr;_finish = nullptr;_end_of_storage = nullptr;}private:iterator _start;//数据起点iterator _finish;//有效数据后一位置iterator _end_of_storage;//容量};
}

2、void push_back(const T& x)

        尾插的数据类型为T类型,有可能是自定义类型。自定义类型传值传参会进行深度拷贝影响代码效率,所以在函数参数部分直接加上引用来提高代码效率。由于vector在初始化部分没有预先开辟空间,所以在尾插之前需要进行开辟空间,即使提前有开好的空间也要进行容量的检查。

        这里直接略过capacity()和size()的说明,这两个函数比较简单。

		void push_back(const T& x){if (_start == _finish){size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();reserve(new_capacity);}*_finish = x;++_finish;}size_t capacity()const{return _end_of_storage - _start;}size_t size()const{return _finish - _start;}

        使用reserve(size_t n)进行空间的开辟。在最开始时容量为0时,默认开辟4个T类型大小的空间。如果该容器之前存在容量,那么就按二倍当前容量进行扩容。

3、void reserve(size n) 

        这里需要明确n是n个T类型的意思。也就是说要开辟的空间是sizeof(T)*n的大小。但是一般开辟空间直接使用new关键字就行。

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

        如果vector中有历史数据就要进行数据的拷贝,这里使用memcpy()库函数来进行实现,需要注意拷贝的字节大小为sizeof(T) * size()。创建old_size变量是为了修复直接给_finish = _start + size()引发的_finish恒等于nullptr的bug.

        假设这里_finish = _start + size(),那么

        在计算_finish时,size() = _finish - _start,此时_start已经指向tmp开辟出来的空间,但是_finish还是nullptr,相减之后size() = 负的_start。所以执行_finish  = _start + 负的_start 就恒为nullptr。在插入数据*_finish = x就会发生错误。解决办法是在_start指向新开辟的空间之前就记录老的size(),再进行后续的计算。 这里也能说明为什么要在析构函数里对_start进行释放。

 4、迭代器

        迭代器iterator就是T*,因为顺序结构的原因直接解引用就能访问到T类型的数据。

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

5、T& operator[](size_t pos)

        方括号要实现的功能就是对容器内数据的访问,对于顺序结构来说,对迭代器本身进行解引用就是数据本身。所以,代码实现就可以写成下面的形式:需要包括只读和读写两个版本:

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

6、iterator insert(iterator pos,const T& x)

        在pos位置插入一个新的数据x,类似于string的插入,但是这里使用迭代器解决了string在sisze_t pos 位置插入数据时挪动数据引发的整型提升导致的错误。但是又带了一个新的问题(迭代器失效),我们先按照正常思路进行代码的实现。

		void insert(iterator pos, const T& x){assert(pos >= _start && pos <= _finish);//扩容逻辑if (_finish == _end_of_storage){size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;reserve(new_capacity);}//挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;}

        按照正常的逻辑,首先要判断pos位置的合法性。其次要检查容器的容量是否充足,容量不够时要进行扩容操作。然后需要对pos位置到_finish之前的数据往后挪动1个位置。最后在pos位置插入新的数据x。

        这样的逻辑看似没有任何问题,但是在测试中不难发现每当容器的容量不够时,再进行扩容操作之后就是发生迭代器失效

        此时我们看到程序没有在容器的头部插入数字5,到底是怎一回事呢?下面我来画图说明问题:

        扩容前容器的容量为4,在头插数据对容器进行扩容之后,依据前面代码的逻辑会开辟新的空间,并进行数据的复制,再释放旧的空间。也就是说,开辟新的空间之后所有的成员属性都指向了新的空间,但是pos仍在指向已经被释放的空间,此时pos就是一个野指针。在对数据进行挪动时

    //挪动数据iterator end = _finish - 1;while (end >= pos)//pos指向原来的旧空间{*(end + 1) = *end;--end;}

         为了解决这一问题,我们应该在扩容开始之前记录pos与_start的相对位置,再扩容结束之后更新pos的位置,从而解决问题。修改后的代码如下:

		iterator insert(iterator pos, const T& x){assert(pos >= _start && pos <= _finish);//扩容逻辑if (_finish == _end_of_storage){size_t len = pos - _start;size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;reserve(new_capacity);pos = _start + len;}//挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;return pos;}

        经过上述的修改,实际上已经解决了迭代器的内部失效问题,但是在外部调用insert函数之后也有可能引发外部的迭代器失效。这里我们需要注意,在insert()之后谨慎使用迭代器。

7、iterator erase(iterator pos)

        删除pos位置的数据,首先还是要对pos位置的合法性进行检查。pos合法之后挪动数据进行覆盖即可。删除数据之后的迭代器也会失效,因为删除操作会将所有的数据往前挪动,之前的迭代器将不再指向挪动之后的数据。所以,为了删除数据之后正常使用迭代器访问容器中的数据,一般需要返回删除位置的下一个位置的迭代器,也就是pos位置。

		iterator erase(iterator pos){assert(pos >= _start && pos < _finish);//挪动数据iterator it = pos - 1;while (it != _finish){*(it - 1) = *it;++it;}--_finish;return pos;}

8、void resize(size_t n,const T& x = T())  

        resize()函数可以更改容器数据的尺寸大小,大逻辑主要是原尺寸大于n,那么就直接截断数据。如果n大于原始数据尺寸那么在进一步判断是否需要扩容,在不满尺寸的位置插入指定数据。这里需要说明的是,如果没有给定数据,也应当插入默认值也就是在函数实现时要给定缺省值。T为自定义类型时需要调用构造函数创建匿名对象,将创建的对象插入容器中。

		void resize(size_t n, const T& x = T()){if (n < size()){_finish = _start + n;}else{reserve(n);while (_finish != _start + n){*_finish = x;++_finish;}}}

9、拷贝构造函数 

        拷贝构造函数可以自己开空间并进行深拷贝,以实现拷贝构造的功能。这里如何来进行深拷贝,可以采用下面的实现方法:将目标容器中的每一个对象赋值给新的对象,而每一个特定对象就会调用自己本身的赋值运算重载函数进行深拷贝。

		vector(const vector<T>& v):_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){_start = new T[v.size()];//浅拷贝//memcpy(_start, v._start, sizeof(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();}

10、赋值运算符重载 

        采用新式写法,使用传值传参,自动调用拷贝构造tmp对象,再与*this进行交换j即可。

		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> tmp){swap(tmp);return *this;}

11、存放自定义类型的深拷贝问题 

        在前面的扩容、构造中我们使用的数据拷贝均为memcpy,这就存在一个问题,当存放的数据类型为自定义类型数据时memcpy仅仅进行的是浅拷贝。

        假设vector中存放的数据类型为string,当容器容量需要扩容时,mencpy仅仅拷贝的是string对象的各个成员变量,并没有开辟新的空间,这就意味着当扩容过程中,开辟了新的空间,而新的空间中存放的依旧是就空间中的string对象,当_str指向新的空间之后,就空间就会调用析构函数,此时delete[] 也就会将就空间中的string对象进行释放。即使新的空间中存放的是就空间的地址,但是这时就空间内的string对象早已不复存在。 

解决方案:

    for (size_t i = 0; i < v.size(); ++i){//使用赋值 进行深拷贝_start[i] = v._start[i];}

        将每一个对象逐个赋值给新的容器,对象间的赋值就会调用自身的赋值运算符重载,从而进行深拷贝。

12、迭代器区间初始化

        vector支持各种迭代器初始化,不仅仅局限于自身类型的迭代器,那么迭代器区间构造函数就需要增加新的模板参数。

		template<class InputIterator>vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){while (first != last){push_back(*first);++first;}}

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

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

相关文章

基于FPGA的BT656编解码

概述 BT656全称为“ITU-R BT.656-4”或简称“BT656”,是一种用于数字视频传输的接口标准。它规定了数字视频信号的编码方式、传输格式以及接口电气特性。在物理层面上,BT656接口通常包含10根线(在某些应用中可能略有不同,但标准配置为10根)。这些线分别用于传输视频数据、…

关于系统重构实践的一些思考与总结

文章目录 一、前言二、系统重构的范式1.明确目标和背景2.兼容屏蔽对上层的影响3.设计灰度迁移方案3.1 灰度策略3.2 灰度过程设计3.2.1 case1 业务逻辑变更3.2.2 case2 底层数据变更&#xff08;数据平滑迁移&#xff09;3.2.3 case3 在途新旧流程兼容3.2.4 case4 接口变更3.2.5…

Microsoft Power BI:融合 AI 的文本分析

Microsoft Power BI 是微软推出的一款功能强大的商业智能工具&#xff0c;旨在帮助用户从各种数据源中提取、分析和可视化数据&#xff0c;以支持业务决策和洞察。以下是关于 Power BI 的深度介绍&#xff1a; 1. 核心功能与特点 Power BI 提供了全面的数据分析和可视化功能&…

【机器学习】自定义数据集 ,使用朴素贝叶斯对其进行分类

一、贝叶斯原理 贝叶斯算法是基于贝叶斯公式的&#xff0c;其公式为&#xff1a; 其中叫做先验概率&#xff0c;叫做条件概率&#xff0c;叫做观察概率&#xff0c;叫做后验概率&#xff0c;也是我们求解的结果&#xff0c;通过比较后验概率的大小&#xff0c;将后验概率最大的…

AMS仿真方法

1. 准备好verilog文件。并且准备一份.vc文件&#xff0c;将所有的verilog file的路径全部写在里面。 2. 将verilog顶层导入到virtuoso中&#xff1a; 注意.v只要引入顶层即可。不需要全部引入。实际上顶层里面只要包含端口即可&#xff0c;即便是空的也没事。 引入时会报warni…

OpenAI o3-mini全面解析:最新免费推理模型重磅发布

引言 2025年1月31日&#xff0c;OpenAI重磅发布全新推理模型o3-mini。这款模型作为OpenAI推理系列的最新突破&#xff0c;不仅在性能和性价比方面实现跨越式提升&#xff0c;更是首次全面开放免费使用。这一重大举措彰显了OpenAI在人工智能技术普及和成本优化领域的创新决心。…

文件读写操作

写入文本文件 #include <iostream> #include <fstream>//ofstream类需要包含的头文件 using namespace std;void test01() {//1、包含头文件 fstream//2、创建流对象ofstream fout;/*3、指定打开方式&#xff1a;1.ios::out、ios::trunc 清除文件内容后打开2.ios:…

TensorFlow 示例摄氏度到华氏度的转换(一)

TensorFlow 实现神经网络模型来进行摄氏度到华氏度的转换&#xff0c;可以将其作为一个回归问题来处理。我们可以通过神经网络来拟合这个简单的转换公式。 1. 数据准备与预处理 2. 构建模型 3. 编译模型 4. 训练模型 5. 评估模型 6. 模型应用与预测 7. 保存与加载模型 …

99.24 金融难点通俗解释:MLF(中期借贷便利)vs LPR(贷款市场报价利率)

目录 0. 承前1. 什么是MLF&#xff1f;1.1 专业解释1.2 通俗解释1.3 MLF的三个关键点&#xff1a; 2. 什么是LPR&#xff1f;2.1 专业解释2.2 通俗解释2.3 LPR的三个关键点&#xff1a; 3. MLF和LPR的关系4. 传导机制4.1 第一步&#xff1a;央行调整MLF4.2 第二步&#xff1a;银…

此虚拟机的处理器所支持的功能不同于保存虚拟机状态的虚拟机的处理器所支持的功能

1.问题&#xff1a;今天记录下自己曾经遇到的一个问题&#xff0c;就是复制别人虚拟机时弹出来的一个报错&#xff1a; 如图&#xff0c;根本原因就在于虚拟机版本的问题&#xff0c;无法对应的上&#xff0c;所以必须升级虚拟机。 2.问题解决&#xff1a; 1.直接点击放弃,此时…

Linux命令入门

Linux命令入门 ls命令 ls命令的作用是列出目录下的内容&#xff0c;语法细节如下: 1s[-a -l -h] [Linux路径] -a -l -h是可选的选项 Linux路径是此命令可选的参数 当不使用选项和参数,直接使用ls命令本体,表示:以平铺形式,列出当前工作目录下的内容 ls命令的选项 -a -a选项&a…

10 Flink CDC

10 Flink CDC 1. CDC是什么2. CDC 的种类3. 传统CDC与Flink CDC对比4. Flink-CDC 案例5. Flink SQL 方式的案例 1. CDC是什么 CDC 是 Change Data Capture&#xff08;变更数据获取&#xff09;的简称。核心思想是&#xff0c;监测并捕获数据库的变动&#xff08;包括数据或数…

【Redis】set 和 zset 类型的介绍和常用命令

1. set 1.1 介绍 set 类型和 list 不同的是&#xff0c;存储的元素是无序的&#xff0c;并且元素不允许重复&#xff0c;Redis 除了支持集合内的增删查改操作&#xff0c;还支持多个集合取交集&#xff0c;并集&#xff0c;差集 1.2 常用命令 命令 介绍 时间复杂度 sadd …

happytime

happytime 一、查壳 无壳&#xff0c;64位 二、IDA分析 1.main 2.cry函数 总体&#xff1a;是魔改的XXTEA加密 在main中可以看到被加密且分段的flag在最后的循环中与V6进行比较&#xff0c;刚好和上面v6数组相同。 所以毫无疑问密文是v6. 而与flag一起进入加密函数的v5就…

【etcd】二进制安装etcd

由于生产服务器不能使用yum 安装 etcd ,或者 安装的etcd 版本比较老&#xff0c;这里介绍一个使用二进制安装的方式。 根据安装文档编写一个下载脚本即可 &#xff1a; 指定 etcd 的版本 提供了两个下载地址 一个 Google 一个 Github&#xff0c; 不过都需要外网 注释掉删除保…

MediaPipe与YOLO已训练模型实现可视化人脸和手势关键点检测

项目首页 - ZiTai_YOLOV11:基于前沿的 MediaPipe 技术与先进的 YOLOv11 预测试模型&#xff0c;精心打造一款强大的实时检测应用。该应用无缝连接摄像头&#xff0c;精准捕捉画面&#xff0c;能即时实现人脸检测、手势识别以及骨骼关键点检测&#xff0c;将检测结果实时、直观地…

学术总结Ai Agent中firecrawl(大模型爬虫平台)的超简单的docker安装方式教程

之前开源了学术总结ai agent&#xff0c;但是对非计算机专业来说&#xff0c;门槛有点高&#xff0c;再加上docker hub镜像被屏蔽&#xff0c;更是不容易上手啊。也有考虑用dify或者扣子去复刻一个&#xff0c;但是从专业用户的角度出发通过界面来拖拽配置实在是不高效&#xf…

交易股指期货有什么技巧吗?

交易股指期货有啥窍门呢&#xff1f;其实啊&#xff0c;追涨杀跌这招&#xff0c;虽然能挣点小钱&#xff0c;但风险也不小&#xff0c;一不小心就可能亏大了。我说的追涨杀跌&#xff0c;不是那种天天追着价格跑的小打小闹&#xff0c;而是要看大趋势&#xff0c;做宏观操作。…

Java线程认识和Object的一些方法ObjectMonitor

专栏系列文章地址&#xff1a;https://blog.csdn.net/qq_26437925/article/details/145290162 本文目标&#xff1a; 要对Java线程有整体了解&#xff0c;深入认识到里面的一些方法和Object对象方法的区别。认识到Java对象的ObjectMonitor&#xff0c;这有助于后面的Synchron…

linux 函数 sem_init () 信号量、sem_destroy()

&#xff08;1&#xff09; &#xff08;2&#xff09; 代码举例&#xff1a; #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h>sem_t semaphore;void* thread_function(void* arg) …