初识C++ · 模拟实现vector

目录

前言:

1 部分简单函数的实现

2 push_back和pop_back

3 reserve和resize

4 Print_vector

5 insert和erase

6 拷贝构造

7 构造

8 赋值

9 memcpy的问题

10 迭代器失效


前言:

继上文模拟实现了string之后,接着就模拟实现vector,因为有了使用string的基础之后,vector的使用就易如反掌了,所以这里直接就模拟实现了,那么实现之前,我们先看一下源代码和文档:

源码不多看,因为很容易绕晕进去,我们从这两个部分得到的信息就够了,首先,vector是一个类模板,即我们可以往里存放许多不同的数据,比如实现一个二维数组,我们可以用vector里面套一个vector,就实现了一个二维数组,为什么说二维数组是一维数组的集合,因为vector的每个空间的start指针指向的空间又是一块连续的空间,即这就是二维数组的构造。

在public的下面有一堆的typedef,这也是不要多看源码的一个原因,很容易绕进去的,比如vector的成员变量的类型是iterator,我们看到value_type*是重命名为iterator了,那么value_type又是T的重命名,即start的类型就是T*,现在我们搞懂了一个问题即模拟实现vector的时候成员变量的类型是模板指针。

那么现在就需要搞懂三个成员变量是什么?

这里就明说了,start是空间的起始位置,finish是空间的最后一个数据的下一个位置,end_of_storage是空间的最后一个位置,所以我们实现size(),capacity()的时候就两两相减就可以了。

基本类型我们了解了,现在就开始模拟实现吧。


1 部分简单函数的实现

这里实现以下函数,size(),capacity(),begin(),end()以及const版本,operator[]以及const版本,empty(),析构函数,注意的就是返回值返回类型即可:

iterator begin()
{return _start;
}
iterator end()
{return _finish;
}
const_iterator begin()const
{return _start;
}
const_iterator end()const
{return _finish;
}
size_t size()
{return _finish - _start;
}
size_t capacity()
{return _end_of_storage - _start;
}
T& operator[](size_t pos)
{assert(pos <= _finish);return *(_start + pos);
}
const T& operator[](size_t pos)const
{assert(pos <= _finish);return *(_start + pos);
}
bool empty()
{return _start == _finish;
}
~vector()
{delete[] _start;_start = _finish = _end_of_storage = nullptr;
}

因为是[]重载,返回的是可以修改的类型所以用引用,这里模拟实现的时候一般都返回size_t类型,这里文档里面的原型,最好保持一致。


2 push_back和pop_back

尾插的时候要注意空间的扩容,扩容的判断条件即是_finish = _end_of_storage的时候,扩容方式和前面实现顺序表链表的时候没有什么区别,使用2倍扩容,但是实际上vs下的vector扩容的时候是使用的1.5倍扩容,我们模拟实现的是linux下的vector,在string模拟实现的时候,我们拷贝数据使用的是strcpy,在这里因为是模板,所以不能使用字符串函数,使用内存拷贝,即使用memcpy,当然memset也可以,但是memcpy就可以了,因为不存在空间重叠的问题。

这里有个隐藏的坑,到后面插入string类的时候才会显式出来,这里先不说,目前插入内置类型是没有问题的:

	void push_back(const T& val){//判断扩容if (_finish == _end_of_storage){size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();T* tmp = new T[newcapacity];memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = _start + size();_end_of_storage = tmp + newcapacity;}*_finish = val;_finish++;}

好了,实现好了吗?当然没有,这里也有个坑,即size()的问题,因为实现size()的时候,实现方式是_finish - _start,但是扩容之后,_start的位置已经改变,所以此时size()的实现变成了原来的_finish - 新的_start了,所以会报错,解决方式就是把size()先存起来再使用:

	void push_back(const T& val){//判断扩容if (_finish == _end_of_storage){size_t old_size = size();size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();T* tmp = new T[newcapacity];memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = tmp + old_size;//容易出错_end_of_storage = tmp + newcapacity;}*_finish = val;_finish++;}

这里如果忘了memcpy的使用可以看看文档。

那么push_back就还有一个坑没有解决了,欲知后事如何,请看下去咯。

pop_back的实现相对来说就简单多了,尾删不代表真正的删除数据,指针指向的位置改变就是尾删了,当然,删除操作想要执行就vector就不能为空:

	void pop_back(){assert(!empty());_finish--;}

3 reserve和resize

reserve的实现我们刚才其实已经实现过了,reserve即是扩容,push_back的时候我们已经考虑了扩容,所以代码差不了多少:

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

当然,这里的坑和push_back是一样的,只有在插入自定义类型的时候才会出现,先不急,那么这里的扩容就是把newcapacity换成了n而已,其余的是没有差别的。

resize模拟实现的时候就要注意了,如果resize实现的是缩容,就直接移动_finish指针就好了,如果实现的是扩容,那么默认参数我们怎么给呢?

文档里面记录的缺省值是value_type(),而value_type是typedef之后的T,所以我们可以看成T val = T(),看着是有点奇怪?其实一点都不奇怪,比如:

int main()
{//Test7_vector();int a = int();cout << a;char ch = char();cout << ch;return 0;
}

这种代码运行是完全没有问题的,这里的a是0,这里的ch是'\0',这是一种初始化方式,所以我们传参的时候就不能写成0,因为不是所有的vector都是vevtor<int>类型的,所以这里我们采用文档的方式,使用这种缺省值,那么扩容的时候多余的空间就给上这个缺省值就好了:

void resize(size_t n, const T& val = T())
{if (size() < n){//扩容reserve(n);while (_finish < _start + n){*_finish = val;_finish++;}}else{//删减_finish = _start + n;}
}

实际上我们也可以一直尾插T(),但是我们既然知道了我们要多少空间,我们不如直接把空间开完,然后进行赋值就好了,赋值的循环条件是_finish指针到最后扩容的指针的位置,也是比较好理解的。


4 Print_vector

当插入和扩容操作都完成了,下一步就是打印了,当然会有人说初始化怎么办,不急,我们先构造一个空的vector就可以了:

vector()
{}iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;

先使用上缺省值,方便测试即可。

Print_vector是用来打印vector的,这里其实变相的考了遍历方式,遍历就是三件套:

void Print_vector(const T& t)
{for (auto e : t){cout << e << " ";}cout << endl;//typename 告诉编译器这是个类型typename vector<T>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}for (int i = 0;i < size();i++){cout << v[i] << " ";}
}

这是三种遍历方式,下标访问和迭代器使用都是没有问题的,唯独需要注意的是typename那里,如果那里我们使用auto it = v.begin()是没有问题的,但是如果我们使用vector<T>::iterator it = v.begin()就会有问题了,这里是因为编译器不知道这里的vector<T>::iterator是类型还是变量了,编译器的原则是不会去类模板里面找东西,而这里使用了模板,那么从编译器的角度出发,它不知道这个究竟是个类型还是变量,所以使用typename。


5 insert和erase

push_back和pop_back是尾插尾删,想要任意位置插入就需要用到insert和erase,文档里面erase有删除一段区间的,这里我们就实现任意位置删除一个数据即可,因为删除一段区间的原理是一样的,因为是任意位置删除和插入,所以实现了之后在push_back和pop_back上也可以复用

insert实现的原理很简单,挪动数据添加数据即可,当然要注意是否要扩容:

void insert(iterator pos, const T& val)
{assert(pos <= _finish);assert(pos >= _start);if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());pos = len + _start;//易错点}iterator it1 = _finish - 1;while (it1 >= pos){*(it1 + 1) = *it1;it1--;}*pos = val;_finish++;
}

当然,为了位置的合法性,这里使用assert断言一下,然后就是判断扩容,但是这里和push_back的size是一个道理,一旦发生扩容,那么pos位置就会变化,while循环的判断就会出问题,所以我们扩容后要更新一下pos的位置,这样才能保证循环的进行。

这里相对string还有一个很好的地方就是我们不用担心end >= pos的那种隐式类型转换比较的发生。

erase的实现原理是一样的,把删除位置的数据往后移动即可。

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

那么这里的复用就是:

void push_back(const T& val)
{insert(end(), val);
}
void pop_back()
{assert(!empty());_finish--;
}

6 拷贝构造

这里的拷贝构造没有string拷贝那么复杂了,不用想深拷贝的问题,开空间一直插入就可以了:

vector(const vector<T>& v)
{reserve(capacity());for (auto& e : v){push_back(e);}
}

拷贝构造的最基本要求不能忘记,参数必须是const 的引用类型,不然会发生无限递归的问题。


7 构造

构造是最终boss。

第一个构造,即构造一个空的顺序表,我们可以专门写个函数:

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

当然,没有必要,我们给上三个缺省值就可以了,这是一个构造重载。

第二个重载是给n个element,也很好操作,一直尾插就好了:

vector(size_t n, const vector<T>& val = T())
{reserve(n);for (int i = 0;i < n;i++){push_back(val);}
}

第二个也没多大问题,再来看第三个区间构造,实际上第三个重载的最后一个参数可以完全不用管的,没有多大意义,区间构造会再用到一个模板,即类模板的成员函数也可以是个模板:

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

以为结束了?还没有,看这段代码:

vector<int> v3 = { 1,2,3,4,5,6 };

这个构造是不是觉得有点奇怪,好像对不上上面三个构造中的任何一个,因为花括号里面的类型是initializer_list

这是一个类,它的成员变量也就几个迭代器和size,但是这是c++11引进的,即c++11准许如下的初始化方式:

void Test_vector()
{//有关initializer_listint a = 1;int b = { 1 };int c{ 1 };//上面写法均正确 但是不推荐后面两种vector<int> v1 = { 1,2,3,4,5,6 };//隐式类型转化vector<int> v2({ 1,2,3,4,5,6,7,8 });//直接构造
}

但是不推荐这种方式,代码可读性没那么高,但是我们看到这种初始化方式应该知道这是使用的initializer_list类型来初始化的,这里的初始化也是涉及到了隐式类型转换,先构造一个临时对象,再拷贝构造这个临时对象,那么编译器会优化为直接构造。

所以构造就是这样构造的:

vector(initializer_list<T> il)
{reserve(il.size());for (auto& e : il){push_back(e);}
}

但是,这里会出问题了:

vector(initializer_list<T> il)
{reserve(il.size());for (auto& e : il){push_back(e);}
}vector(size_t n, const vector<T>& val = T())
{reserve(n);for (int i = 0;i < n;i++){push_back(val);}
}void Test()
{vector<int> v(10,1);vector<int> v(10u,1);vector<int> v(10,'a');
}

下面的两个构造就不会出问题,第二个的u是无符号的标志,即size_t,第一个会出问题,因为10和1默认的类型是int,那你说,调用上面的没问题吧?调用下面的,虽然第一个的参数类型是size_t,但是将就将就吧也可以,那你说,编译器调用哪个?

所以这里会报错,那么源代码里面解决的就很简单粗暴:

vector(int n, const vector<T>& val = T())
{reserve(n);for (int i = 0;i < n;i++){push_back(val);}
}

类型转换一下~ 


8 赋值

这里的赋值我们可以直接采用现代写法,不用传统的先开空间,再进行数据拷贝,我们利用swap函数,可以达到很好的效果:

	void swap(const vector<T>& v){std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_end_of_storage,v._end_of_storage);}

这里需要注意到的是swap的调用需要用到域名访问限定符,不然编译器会调用离函数最近的函数,即它本身,而我们使用的是std库里面的交换函数,所以需要使用::。

有了交换,我们构造一个临时对象,再进行交换即可:

vector<int>& operator=(vector<T> v)
{swap(v);return *this;
}

我们提及到过连续赋值的条件是返回值应该是赋值过后的值,所以我们需要返回赋值之后的对象。


9 memcpy的问题

先看这样一段代码:

void Test5_vector()
{vector<string> v;v.push_back("11111");	v.push_back("22222");v.push_back("33333");v.push_back("44444");v.push_back("55555");for (auto& e : v){cout << e << " ";}cout << endl;
}

如果我们只存入4个或者及以下的数据是没有问题的,但是存入5个数据之后,就会存在访问出错的问题,这一切都是因为memcpy,也就是在扩容,尾插的时候会涉及到的问题。

我们了解memcpy的底层之后,就知道memcpy拷贝的方式是逐字节的拷贝,所以当_start指向的空间是自定义类型的时候,经过扩容之后,就会导致_start指向的空间被销毁,但是拷贝过来的指针依旧是指向那块被销毁的空间的,我们再次访问自然就会报错。

所以这里我们最好的方法就是进行赋值:

void reserve(size_t n)
{if (capacity() < n){size_t old_size = size();T* tmp = new T[n];//memcpy(tmp, _start, sizeof(T) * size());for (int i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

本质就是拷贝构造一个临时对象,就不会导致越界访问的问题。


10 迭代器失效

void Test6_vector()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);v1.push_back(6);v1.push_back(7);v1.push_back(8);vector<int>::iterator it = v1.begin() + 3;v1.insert(it, 40);Print_vector(v1);//it = v1.begin() + 3;cout << *it << endl;
}

当我们多次插入之后,伴随着扩容的发生,所以it的位置会发生改变,it指向的是原来的空间位置,所以此时我们打印it的值的时候就会打印出来错误的值,所以如果发生了空间的改变导致迭代器失效,如果我们还要使用迭代器,我们就需要重置迭代器-》即那段注释。

void Test7_vector()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(4);v1.push_back(5);v1.push_back(4);vector<int>::iterator it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){v1.erase(it);it++;}}for (auto e : v1){cout << e << " ";}cout << endl;
}

且看这段删除偶数的代码,好像没有问题?有以下几种情况就会出错了,11252,1223。

因为删除之后会有移动数据的发生,比如1223,删除了第一个2,第二个2往前面移动,然后再pos位置的指针往后移动,就相当于it指针移动了两次,所以会错过,那么如果最后一个是偶数,即11252,就会完美错过end,那么循环条件就无法结束,就会死循环了。

解决方法也很简单,让it删除之后原地踏步就好了:

while (it != v1.end())
{if (*it % 2 == 0){it = v1.erase(it);//解决方法}else{++it;}
}

这也是为什么erase的返回值是iterator的原因,整个代码都是前后呼应的。


感谢阅读!

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

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

相关文章

MyBatis复习笔记

3.Mybatis复习 3.1 xml配置 properties&#xff1a;加载配置文件 settings&#xff1a;设置驼峰映射 <settings><setting name"mapUnderscoreToCamelCase" value"true"/> </settings>typeAliases&#xff1a;类型别名设置 #这样在映射…

如何去除视频上的文字?免费无痕去水印分享!视频制作良器!

对于需要进行二次创作的视频素材&#xff0c;去除原有的文字可以提供一个更加干净的画布&#xff0c;方便创作者在其基础上进行新的创作和编辑。同时&#xff0c;去除文字后的视频也更方便分享到各种平台&#xff0c;避免因为平台对文字的限制而导致视频无法发布或传播。 要去除…

云计算期末复习(1)

云计算基础 作业&#xff08;问答题&#xff09; &#xff08;1&#xff09;总结云计算的特点。 透明的云端计算服务 “无限”多的计算资源&#xff0c;提供强大的计算能力 按需分配&#xff0c;弹性伸缩&#xff0c;取用方便&#xff0c;成本低廉资源共享&#xff0c;降低企…

Windows操作系统基本知识整理

目录 引言 一、Windows操作系统的发展历史 1.1 Windows 1.0到Windows 3.0 1.2 Windows 95到Windows Me 1.3 Windows NT到Windows 2000 1.4 Windows XP到Windows 7 1.5 Windows 8到Windows 10 二、Windows操作系统的核心组件 2.1 内核 2.2 文件系统 2.3 图形用户界面&…

内网横向移动小补充 --->PTK

大家别急&#xff0c;我的基于资源的约束性委派攻击还在写&#xff0c;这个东西一时半会讲不清楚&#xff0c;所以我在这里先来补充一点横向移动以前没说好的东西&#xff01;&#xff01;&#xff01; 在更啦&#xff0c;别催啦~~~~ 还记得我之前在内网渗透里面讲过这个PTK&a…

亚马逊云主管马特·加尔曼面临压力,致力于在人工智能领域赶超竞争对手

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

mysql中连接查询的成本

大家好。上篇文章我们讲了mysql中成本的含义以及单表查询如何计算成本。现在我们接着讲讲mysql中连接查询的成本。 在讲之前&#xff0c;我们先创建两张一样的表single_table和single_table2&#xff0c;并在表中插入10000条数据。在下面的讲解中&#xff0c;我们称single_tab…

java并发工具类都有哪些

Java中的并发工具类包括&#xff1a; CountDownLatch CountDownLatch允许一个或多个线程等待其他线程完成某些操作。它通常用于线程间的同步&#xff0c;例如在一个线程完成其工作后通知其他线程继续执行。 CyclicBarrier CyclicBarrier是一个同步辅助类&#xff0c;它允许一…

【面试必看】Java并发

并发 1. 线程 1. 线程vs进程 进程是程序的一次执行过程&#xff0c;是系统运行程序的基本单位&#xff0c;因此进程是动态的。 系统运行一个程序即是一个进程从创建&#xff0c;运行到消亡的过程。在 Java 中&#xff0c;当我们启动 main 函数时其实就是启动了一个 JVM 的进…

ChaosMeta V0.7.0 版本发布 进入CNCF混沌工程全景图

混沌工程 ChaosMeta 的全新版本 V0.7.0 现已正式发布&#xff01;该版本包含了许多新特性和增强功能&#xff0c;在编排界面提供了多集群管理&#xff0c;在代码层面支持多命令下发通道的选择。另外由蚂蚁集团发起的ChaosMeta于北京时间2024年1月10日正式进入CNCF混沌工程全景图…

07_Servlet

Servlet 一 Servlet简介 1.1 动态资源和静态资源 静态资源 无需在程序运行时通过代码运行生成的资源,在程序运行之前就写好的资源. 例如:html css js img ,音频文件和视频文件 动态资源 需要在程序运行时通过代码运行生成的资源,在程序运行之前无法确定的数据,运行时动态生成…

转行一年了

关注、星标公众号&#xff0c;直达精彩内容 ID&#xff1a;技术让梦想更伟大 整理&#xff1a;李肖遥 来公司一年了。 说是转行其实还是在半导体行业&#xff0c;熟悉我的朋友知道 &#xff0c;我在18年开始进入半导体行业&#xff0c;那个时候想着行业很重要&#xff0c;站对了…

气泡水位计的安装方法详解(二)

气泡水位计的安装方法详解&#xff08;二&#xff09; 产品简介 气泡式水位计ZL-BWL-013是一款适用于水文、水利信息化建设领域的新一代水位测量类设备&#xff0c;产品执行GB/T 11828.2-2022标准。ZL-BWL-013气泡水位计&#xff0c;具有安装方便、易于操作&#xff0c;高精度…

算法刷题day54:搜索(一)

目录 引言一、池塘计数二、城堡问题三、山峰和山谷四、迷宫问题五、武士风度的牛六、抓住那头牛七、矩阵距离八、魔板 引言 针对于蓝桥杯&#xff0c;搜索问题还是非常之重要的&#xff0c;在省赛前深知暴搜的重要性&#xff0c;所以提前先把提高课的搜索一章给看了&#xff0…

分布式锁的原理和实现(Go)

文章目录 为什么需要分布式锁&#xff1f;go语言分布式锁的实现Redis自己的实现红锁是什么别人的带红锁的实现 etcdzk的实现 为什么需要分布式锁&#xff1f; 保证分布式系统并发请求或不同服务实例操作共享资源的安全性&#xff0c;通过一种协调机制来保证在同一时刻只有一个…

设计模式17——模板方法模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 模板方法模式&#xff08;Temp…

阿里云Linux 3.2104 LTS 64位安装SVN服务器

直接按步骤 yum install subversion 写y就行 主要是看看安装了那些文件 rpm -ql subversion 主要是为了创建版本库而准备&#xff0c;这个能一遍创建就一遍创建&#xff0c;不行就逐个创建。能创就忽略下面两个mkdir步骤。 mkdir /home/svn/groupRepos 根据新建目录作为版本…

LeetCode第131场双周赛C++题解

3158.求出出现两次数字的XOR值 给你一个数组 nums &#xff0c;数组中的数字 要么 出现一次&#xff0c;要么 出现两次。 请你返回数组中所有出现两次数字的按位 XOR 值&#xff0c;如果没有数字出现过两次&#xff0c;返回 0 。 示例 1&#xff1a; 输入&#xff1a;nums …

业务实战————Uibot6.0 .1多页面商品信息抓取RPA机器人

前言 【案例描述】 鲜果记水果店计划在淘宝电商平台上开设一家新店&#xff0c;小微是该企业运营部分的运营专员&#xff0c;主要负责公司商品上架和管理的工作。 公司计划在开店的新品促销活动中增加水果品类红富士苹果。小微需在商品上架前了解目前平台中销量前列的红富士苹…

数字水印 | 离散余弦变换 DCT 基本原理及 Python 代码实现

目录 1 基本原理2 代码实现3 图像压缩 1 基本原理 参考博客&#xff1a;https://www.cnblogs.com/zxporz/p/16072580.html D C T \mathsf{DCT} DCT 全称为 D i s c r e t e C o s i n e T r a n s f o r m \mathsf{Discrete\ Cosine\ Transform} Discrete Cosine Transfo…