C++ vector类的模拟实现

点击蓝字

b780f3c92f9d701f15239a4daf88c937.png

关注我们

1.前言

vector和string虽然底层都是通过顺序表来实现的,但是他们利用顺序表的方式不同,string是指定好了类型,通过使用顺序表来存储并对数据进行操作,而vector是利用了C++中的泛型模板,可以存储任何类型的数据,并且在vector中,并没有什么有效字符和容量大小的说法,底层都是通过迭代器进行操作的,迭代器底层实现也就是指针,所以说,vector是利用指针对任何顺序表进行操作的。

a5047d04b125a30e59350968c1886a0f.png

2.vector属性

  • _start用于指向第一个有效元素

  • _finish用于指向最后一个有效元素的下一个位置

  • _endOfStorage用于指向已经开辟了的空间的最后一个位置的下一个位置

  • vector的迭代器是原生态T*迭代器

template<class T>
class Vector
{
public:typedef T* iterator;typedef const T* const_iterator;private:iterator _start;iterator _finish;iterator _endOfStorage;
};

3.构造函数

无参默认构造函数,将所有属性都置空

以n个val初始化的构造函数,先开辟n个空间,再将这些空间的值都置为val,并更新_finish和_endOfStorage的位置

通过迭代器传参初始化的构造函数,使用新的迭代器,通过尾插将数据插入到新的空间

使用新的迭代器的原因是使传入的迭代器可以是任意类型的,如果使用Vector的迭代器,那么传入的迭代器的类型只能和Vector的类型一样,这里拿string举例,创建一个char类型的Vector,Vector,但是传入的迭代器并不是char类型的,可以是字符数组的迭代器或者是string的迭代器。只要通过解引用是char类型就可以

//无参默认构造Vector():_start(nullptr),_finish(nullptr),_endOfStorage(nullptr){}//n个val的构造函数Vector(int n, const T& val = T()):_start(new T[n]),_finish(_start +n),_endOfStorage(_finish){for (int i = 0; i < n; ++i){_start[i] = val;}}//通过迭代器产生的构造函数template<class InputIterator>Vector(InputIterator first, InputIterator last):_start(nullptr), _finish(nullptr), _endOfStorage(nullptr){while (first != last){pushBack(*first);++first;}}

运行结果在begin() 和end()实现中

4.size()和capacity()

指针相减得到的值就是这两个指针之间的元素个数

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

9e945925251310a819fc2227a95224c0.png

5.pushBack()

  • 检查容量,如果_finish和_endOfStorage指针相等,说明容量已经满了,需要开辟更大的空间

  • 在_finish位置插入新的数据

  • 更新_finish

void pushBack(const T& val)
{//检查容量if (_finish == _endOfStorage){size_t newC = _endOfStorage == nullptr ? 1 : 2 * capacity();reserve(newC);}//插入数据*_finish = val;//更新finish++_finish}

运行结果在begin() 和end()实现中

6.reserve

  • 检查n的合理性,reserve只能扩大不能缩小空间

  • 保存有效元素的个数,用于后面更新_finish使用

  • 申请空间并将数据拷贝到新的空间中,释放旧的空

  • 更新3个成员变量,注意_finish不能更新为_finish+size(),原因是size()是通过两指针运算得出来的,此时的_fiinsh已经指向了释放的空间,再去使用会出错,所以这也是有第二步的原因

以下代码存在浅拷贝问题,文章末尾会给出正确深拷贝代码和详细解释

void reserve(size_t n)
{//reserve只能扩大空间不能缩小空间if (n > capacity()){//保存有效元素size_t sz = size();//申请空间T* tmp = new T[n];//将数据拷贝到新的空间if (_start != nullptr){//拷贝有效元素memcpy(tmp, _start, sizeof(T) * size());delete[] _start;}//更新_start = tmp;_finish = _start + sz;_endOfStorage = _start + n;}}

运行结果在begin() 和end()实现中

7.begin() 和end()

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

3683edffe5cb9d83776cfb9dfb073d92.png有了begin()和end就可以使用范围for

template<class T>
void printVectorFor(Vector<T>& vec)
{for (auto& e : vec){cout << e;}cout << endl;
}

d5adb7f35950c659d08e10c6c5a6096b.png

8.[]运算符重载

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

94b2231a71f6df3dc165240fe7e4d029.png

9.resize()

  • n <= size 直接更新_finish的位置即可

  • size < n <= capacity,从_finish开始补充元素,补充到_start+n的位置,然后执行第一步

  • n > capacity 增容,执行第二和第一步

void resize(size_t n, const T& val = T())
{//3.n >= capacityif (n > capacity()){reserve(n);}//2.size < n <= capacityif (n > size()){while (_finish != _start + n){*_finish = val;++_finish;}}//1.n<=size_finish = _start + n;}

d2d9aed3fbb4b1a8e9cc556f134988ee.png

10.insert()

  • 检查插入的位置的有效性[_start, _finish)

  • 检查容量,由于增容会导致pos迭代器失效,所以我们可以先保存pos对于_start的偏移量offset,增容后,再将pos重新赋值pos=_start+offset

  • 移动元素,从后往前移动,最后将pos位置的元素置为val

  • 更新_finish

void insert(iterator pos, const T& val)
{//检查位置有效性assert(pos >= _start || pos < _finish);//检查容量if (_finish == _endOfStorage){//增容会导致迭代器失效//保存pos和_start的偏移量size_t offset = pos - _start;size_t newC = _endOfStorage == nullptr ? 1 : 2 * capacity();reserve(newC);//更新pospos = _start + offset;}//移动元素iterator end = _finish;while (end != pos){*end = *(end - 1);--end;}//插入*pos = val;//更新++_finish;}

e178710fd5e33bb9eb129e871f44b322.png

11.erase()

  • 检查位置有效性

  • 移动元素,从前向后移动

  • 更新_finish

iterator erase(iterator pos)
{//检查位置有效性assert(pos >= _start || pos < _finish);//移动元素,从前往后iterator start = pos + 1;while (start != _finish){*(start - 1) = *start;++start;}//更新--_finish;}

5d5fea009e5acc3aa1ce7e10ba4d2dac.png

12.void popBack()

利用erase接口进行尾删

void popBack(){if (size() > 0)erase(end() - 1);}

fccb7181889b1d9e0f5d0d20e627a593.png

13.析构函数

~Vector(){if (_start){delete[] _start;_start = _finish = _endOfStorage = nullptr;}}

14.算法库中的find

头文件<algorithm>

template <class InputIterator, class T>InputIterator find (InputIterator first, InputIterator last, const T& val)

参数内容(从迭代器的begin起到end中,找到val值,找到返回该值所在的迭代器,找不到返回end)

83e350ddad37d3bdf79c6796d86ee53e.png

15.reserve的深浅拷贝问题

当我门使用自定义类型时,使用浅拷贝是效率最高的,但是当我们使用自定义类型时,并且存在内存资源的利用,就必须时刻注意存在的深浅拷贝问题。来看以下代码测试

void test()
{Vector<string> v;string str1 = "123";string str2 = "456";string str3 = "789";v.pushBack(str1);v.pushBack(str2);v.pushBack(str3);
}

调试结果:

3f251dea12cf8e62e53371aa631143b4.png

当我们在插入第三个字符串时,就发生了内存异常的问题,我们来看看到底是什么问题。
第一次插入str1,没有问题

f3a7e57fb423973e2fa80abc1389aa62.png

第二次插入str2,插入之前我们会扩容,会创建2倍大的空间tmp,然后通过memcpy内存拷贝(浅拷贝)将内容拷贝到tmp中,此时就有两个指向指向一个资源(123),拷贝完后delete[]要删除原有空间,将123释放后,其实现在新的空间的第一个元素指向的是一个已经释放了的空间,但是问题并没有暴露出来,第二个元素的插入也没有问题

88c7e1e51d02207c2cd04601a3eb963b.png

第三次str3的插入,这次插入也会进行扩容,会先开辟一个2倍大的空间tmp,然后通过memcpy内存拷贝(浅拷贝)将内容拷贝到tmp中,此时有两个指针指向已经释放的资源(123),有两个指针指向资源(456),当拷贝完成后会释放旧的空间,当释放原指针指向的(456)时不会报错,原因和第二次插入原因一样。但是释放原有空的第一个指针时,就会发生内存报错异常,原因是资源(123)已经被释放了,如果再释放就属于二次释放,是不安全的。内存错误就报异常。

e177381fdd87203078c267d186dbc58e.png

所以我们在扩容的时候不应该只是单纯的浅拷贝,也就是使用memcpy来拷贝内容,我们应该要使用深拷贝。将memcpy改为for (size_t i = 0; i < sz; ++i){tmp[i] = _start[i];}
整体代码如下:

void reserve(size_t n)
{//reserve只能扩大空间不能缩小空间if (n > capacity()){//保存有效元素size_t sz = size();//申请空间T* tmp = new T[n];//将数据拷贝到新的空间if (_start != nullptr){//拷贝有效元素//memcpy(tmp, _start, sizeof(T) * size());//深拷贝for (size_t i = 0; i < sz; ++i){//调用自定义类型的赋值运算符重载函数,完成深拷贝//前提是该重载函数也是深拷贝,string是STL库中,是被深拷贝处理过tmp[i] = _start[i];}delete[] _start;}//更新_start = tmp;_finish = _start + sz;_endOfStorage = _start + n;}}

44bae889314dfec245e20c6dd76add32.gif

如果你年满18周岁以上,又觉得学【C语言】太难?想尝试其他编程语言,那么我推荐你学Python,现有价值499元Python零基础课程限时免费领取,限10个名额!
▲扫描二维码-免费领取

e68d7763c649cf1322dbd1686f703fd3.gif

戳“阅读原文”我们一起进步

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

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

相关文章

visual studio源文件的编译顺序是依据什么?

问题&#xff1a;visual studio源文件的编译顺序是依据什么&#xff1f; 结论&#xff1a;依据 .vcxproj 文件里 指定了ClCompile的ItemGroup &#xff0c;如下图所示&#xff0c;就是这么简单粗暴。

并发运行的最佳实践_并发最佳实践

并发运行的最佳实践本文是我们名为“ 高级Java ”的学院课程的一部分。 本课程旨在帮助您最有效地使用Java。 它讨论了高级主题&#xff0c;包括对象创建&#xff0c;并发&#xff0c;序列化&#xff0c;反射等。 它将指导您完成Java掌握的旅程&#xff01; 在这里查看 &#…

功能齐全的屏幕截图C++实现详解

点击蓝字关注我们1、概述要使用屏幕截图&#xff0c;其实很容易&#xff0c;装一款聊天软件或者办公软件就可以了&#xff0c;比如QQ、企业微信、钉钉、飞书等。但要开发出类似这些软件的屏幕截图模块&#xff0c;则没那么容易。其实实现屏幕截图的技术并不复杂&#xff0c;主要…

如何判断exe文件是debug还是release编译生成的

如何判断exe文件是debug还是release编译生成的结论&#xff1a; 用IDA工具打开exe&#xff0c;然后看Imports里面的依赖库是否有带d或D结尾的&#xff0c;如果有就说明是Debug的 实验&#xff1a;&#xff08;实验环境 vs2017&#xff0c; IDA工具&#xff09; &#xff08;0&…

大屏可视化分配率是什么意思_什么是分配率?

大屏可视化分配率是什么意思诸如“不可持续的分配率”和“您需要保持较低的分配率”之类的短语似乎仅属于Java Champions的词汇表。 复杂&#xff0c;恐怖并被魔术光环包围。 经常发生的情况是&#xff0c;当您更仔细地查看概念时&#xff0c;魔术会随着抽烟消失。 这篇文章试…

C/C++语言动态开辟的杨辉三角

点击蓝字关注我们问题引入杨辉三角相必大家并不陌生&#xff0c;第1行有1列、第二行有2列…第n行有n列&#xff0c;且每行行首和行尾的值都为1&#xff0c;其余的值为上一行两数相加我们在C语言阶段&#xff0c;第一次碰到的杨辉三角应该都是用常规的二维数组存储&#xff0c;可…

git gui 历史版本_这些Git命令都不会,还是不要去面试了

前言以下&#xff0c;项目中经常使用的Git命令&#xff0c;汇总到这里以便与你能快速的学习和掌握Git命令&#xff0c;在文章最后有惊喜哟&#xff0c;一定要看到最后啊&#xff01;使用的 Git版本&#xff1a;git version 2.24.0命令git log# 输出概要日志,这条命令等同于# gi…

java restful_Java EE中的RESTful计时器

java restful在这篇文章中...。 EJB计时器旋风之旅 通过带有示例实现的简单REST接口即时使用EJB计时器 更新&#xff08;2015年7月14日&#xff09; 该应用程序的前端现在可以在OpenShift上使用 。 由于我是前端新手&#xff0c;因此我在其他来源的帮助下组装了此HTML5 Ang…

【lua学习】1.源码组织

虚拟机核心相关文件列表内嵌库相关文件解释器&#xff0c;字节码编译器相关的文件做cocos2d-x lua已经有一段时间了&#xff0c;想更深入了解lua。我会出一系列地 自身学习过程中地解读。我会带大家沿着源码来逐步解读lua&#xff0c;我喜欢按照 深度优先遍历的顺序来解读源码&…

c# 联合halcon 基于相关性 模板匹配_机器视觉之halcon入门(5)-字符识别exe生成...

2.3.2 第二个halcon程序转EXE程序&#xff1a;字符识别老规矩&#xff0c;每一段halcon代码得用C#二次开发下。根据上一节所教的&#xff0c;我们配置下C#的环境&#xff0c;顺便添加好控件&#xff0c;如下图(2-3-2-1)。图 2-3-2-1控件基本跟上一节一样&#xff0c;只是少了一…

C语言数据的存储和取出(超详细讲解)

点击蓝字关注我们整形的储存我们知道一个整形的存储是以补码的形式储存取出是原码的形式。比如&#xff1a;int a 5;的二进制是101那它的原码应该是&#xff1a;00000000 00000000 00000000 00000101正数的原反补相同那它存进去和取出来都是&#xff1a;00000000 00000000 000…

go语言 不支持动态加载_动态语言支持

go语言 不支持动态加载本文是我们名为“ 高级Java ”的学院课程的一部分。 本课程旨在帮助您最有效地使用Java。 它讨论了高级主题&#xff0c;包括对象创建&#xff0c;并发&#xff0c;序列化&#xff0c;反射等。 它将指导您完成Java掌握的旅程&#xff01; 在这里查看 &am…

【lua学习】2.数据类型

【lua学习】2.数据类型Lua中的数据类型关于TValue自顶向下分析TValue表示所有的Lua数据结构并带一个类型字段Value表示所有的Lua数据结构GCObject表示所有需要进行垃圾回收的数据结构GCheader表示需要GC的数据结构最开始的部分Lua中的数据类型 宏名 (见lua.h)宏值类型对应数据…

打印pdf就一页_PDF 文件转换工具

是将 PDF 文件转换为完全可编辑的 Windows 文档最好的转换软件。无论您需要您的内容是 Microsoft Word、Excel、PowerPoint、HTML 还是仅需要文本&#xff0c; 总会给您一个简单的方法&#xff0c;快捷地获取您要的内容。可转换整个文档或选择内容。亦可创建 PDF 文件。PDF 转换…

C++类的this指针,静态成员,友元函数友元类

点击蓝字关注我们1. this指针在上篇讲C中类&#xff0c;对象&#xff0c;封装&#xff0c;继承&#xff08;派生&#xff09;&#xff0c;多态的时候&#xff0c;this指针出现在成员函数中&#xff0c;并使用->成员提取符操作成员变量。在 C 中&#xff0c;每一个对象都能通…

jooq和jdbc_将jOOQ与JDBC比较

jooq和jdbc本文是我们学院课程“ jOOQ –类型安全数据库查询”的一部分 。 在SQL和特定关系数据库很重要的Java应用程序中&#xff0c;jOOQ是一个不错的选择。 当JPA / Hibernate抽象过多而JDBC过于抽象时&#xff0c;这是一种替代方法。 它显示了一种现代的领域特定语言如何可…

【lua学习】3.字符串

【lua学习】3.字符串Lua字符串的概况字符串实现字符串结构TString全局字符串表stringtable新建字符串luaS_newlstr &#xff08;先查表&#xff0c;再决定创建与否&#xff09;新建字符串 newlstr重新设置全局字符串的大小 luaS_resize全局字符串表的缩容保留字是如何不被回收的…

【lua学习】4.表

1 概述2 数据结构2.1.表Table2.2 键TKey2.3 节点&#xff08;键值对&#xff09;Node3 操作算法3.1 查找3.1.1 通用查找luaH_get3.1.2 根据字符串查找 luaH_getstr3.1.3 根据整数查找 luaH_getnum3.2 新增元素/修改元素/删除元素 luaH_set系列3.2.1 根据key获取或创建一个value…

批量提取文件创建时间_批量采集新浪微博用户内容

有时我们需要把某些用户的微博数据全部采集下来用作分析&#xff0c;每条信息复制的工作量是非常低效的&#xff0c;必须要借助工具。今天给大家介绍一款采集软件&#xff1a;微风采集器。打开软件&#xff0c;选择模板&#xff0c;下拉框选&#xff1a;批量提取指定用户微博内…

C++异常的规则

点击蓝字关注我们异常是指存在于程序运行时的异常行为&#xff0c;这些行为超出了函数正常功能的范围&#xff0c;当程序的某部分检测到一个无法处理的问题时&#xff0c;就需要用到异常处理。1. C语言中传统的处理错误方式终止程序&#xff1a;如assert&#xff0c;当发生错误…