【C++初阶】从零开始模拟实现vector(含迭代器失效详细讲解)

目录

1、基本结构

1.1成员变量

 1.2无参构造函数

1.3有参构造函数

preserve()的实现

代码部分: 

push_back()的实现 

代码部分:

代码部分:

1.4拷贝构造函数

代码部分:

1.5支持{}初始化的构造函数

 代码部分:

 1.6支持迭代器区间初始化的构造函数

代码部分:

 1.7支持一次插入多个相同元素的构造函数

代码部分:

resize()函数的实现:改变大小 

代码部分:

1.8析构函数

代码部分: 

2、插入和删除

2.1尾插

代码部分: 

2.2在任意位置插入

代码部分: 

 2.3尾删

代码部分:

2.4在任意位置删除 

思路:

代码部分: 

3、运算符的重载

3.1赋值运算符的重载

1、传统写法

代码部分: 

2、现代写法

swap()的实现

代码部分: 

3.2operator []的实现

代码实现:

4、迭代器失效问题(重点)

4.1迭代器失效的原因和后果

4.2常见迭代器失效的操作

4.2.1扩容相关操作

4.2.2删除相关操作

总结:


1、基本结构

1.1成员变量

vector类有三个成员变量,_start指向数据块起始位置,_finish指向有效数据块的结尾位置,_end_of_storage指向有效空间容量的结束位置

在这里给它们去缺省值

namespace yyc
{template <class T>class vector{public:typedef T* iterator;private://成员变量初始化为空iterator _start = nullptr;//指向数据块起始位置iterator _finish = nullptr;//指向有效数据的尾iterator _end_of_storage = nullptr;//指向空间容量的尾};
}

 1.2无参构造函数

这里的无参构造函数,也可以不写初始化列表(因为成员变量给的缺省值会自动给初始化列表)

vector()
{} 
//vector() 
//: _start(nullptr)
//, _finish(nullptr)
//, _endOfStorage(nullptr) 
//{}

1.3有参构造函数

初始化一个给定大小的vector,并使用默认值填充

在实现构造之前,我们可以实现一个扩容成员函数reserve()和一个尾插成员函数push_back()

preserve()的实现

实现reserve的过程还可以实现size()capacity()函数

在实现preserve时要注意的是:

1、这里用到的并不是memcpy来进行数据拷贝,而是用for循环来一个一个赋值

这是因为在面对T为string类型时,使用memcpy会导致浅拷贝

2、在扩容后要进行迭代器的更新,防止出现迭代器失效的问题

代码部分: 
		size_t size() const{return _finish - _start;}size_t capacity() const{return _end_of_storage - _start;}void reserve(size_t n){if (n > capacity()){size_t oldsize = size();//将原有空间中有效数据长度记录下来T* tmp = new T[n];//memcpy(tmp,_start,sizeof(T)*size())//memcpy进行的是浅拷贝,会导致迭代器失效for (size_t i = 0; i < oldsize; ++i){tmp[i] = _start[i];//将原有空间中的资源转到新开的空间}delete[] _start;//销毁旧空间_start = tmp;_finish = _start + oldsize;//finish重新指向原本要指向的位置_end_of_storage = _start + n;}}

push_back()的实现 

代码部分:
		void push_back(const T& x){if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}*_finish = x;++_finish;}

然后我们直接复用这两个成员函数就可以实现初始化

但要实现范围for就必须先实现两个返回首尾位置的迭代器

代码部分:

		typedef T* iterator;typedef const T* const_iterator;//实现范围foriterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}

通过一系列的复用来实现构造函数,这样就极大的提高代码的简洁性以及易于修改 

代码部分:
		vector(const vector<T>& x){reserve(x.size());for (auto a : x){push_back(a);}}

1.4拷贝构造函数

拷贝构造函数实现的思路和有参构造函数差不多,也是进行复用来实现

代码部分:
		vector(const vector<T>& x){reserve(x.size());for (auto a : x){push_back(a);}}

1.5支持{}初始化的构造函数

 这是C++版本才新出的一种初始化方式

它支持vector<int> v = {1,2,3,4,5};这种方式初始化

 代码部分:

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

初始化实现: 

	yyc::vector<int> v1 = { 1,2,3,4,5,6 };//一种一种隐式类型转换yyc::vector<int> v2({ 1,2,3,4,5,6 });//调用对应构造

 1.6支持迭代器区间初始化的构造函数

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

这种初始化方式就可以实现

yyc::vector<int> v1(10,1);
yyc::vector<int> v2(v1.begin(), v1.begin() + 3);

 1.7支持一次插入多个相同元素的构造函数

可以实现初始化一次插入多个元素

代码部分:
		vector(int n, const T& val = T()):_start(new T[n]),_finish(_start+n),_end_of_storage(_finish){//while (n--)//{//	push_back(val);//}for (int i = 0; i < n; ++i){_start[i] = val;}}

初始化实现: 

yyc::vector<int> v1(10,1);

但还有一种简洁的方式实现该构造函数

1、一种是复用reserve()和push_back()l来实现

2、直接复用resize()函数

resize()函数的实现:改变大小 

代码部分:
		void resize(size_t n, const T& val = T())//这里的的val给缺省值{if (n <= size())//当n<=size()时,会进行数据的删除{_finish =_start;return;}else if (n > capacity())//当n>capacity()时,就进行扩容{reserve(n);//复用写好的reserve()//多出来的空间插入数据while (_finish != _start + n){//从原本的_finish处进行尾插*_finish = val;++_finish;}}}

复用resize(),通过n的大小来进行数据的插入 

vector(size_t n, const T& val = T())  //  这里我们实现了两个函数  
{										//  一个是size_t 的 n  一个是intresize(n, val);						// 可以有效的匹配不同调用场景
}										// 防止负数隐式转换
vector(int n, const T& val = T())
{resize(n, val);
}

1.8析构函数

代码部分: 
		~vector(){delete[] _start;_start = _finish = _end_of_storage = nullptr;}

2、插入和删除

2.1尾插

代码部分: 
		void push_back(const T& x){if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}*_finish = x;++_finish;}

2.2在任意位置插入

 注意事项:

当插入过程中需要扩容时,就需要进行迭代器的更新,防止迭代器失效

代码部分: 
		void insert(iterator pos, const T& val){assert(pos >= _start);assert(pos <= _finish);if (_finish == _end_of_storage){//保留pos位置,防止扩容后迭代器失效size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;//更新迭代器}iterator it = _finish - 1;while (it >= pos){*(it + 1) = *it;//挪动数据--it;}*pos = val;//在指定位置插入数据++_finish;}

 2.3尾删

代码部分:
		void pop_back(){assert(_finish > _start);--_finish;}

2.4在任意位置删除 

思路:

将pos+1后面的数据一个一个往前移,进行数据的覆盖

最后返回删除位置的迭代器,以此来更新迭代器

代码部分: 
		iterator erase(iterator pos)//删除指定位置数据{assert(pos >= _start);assert(pos <= _finish);//用后面的数据覆盖前面的数据iterator it = pos + 1;while (it < _finish){*(it - 1) = *it;it++;}_finish--;return pos;}

3、运算符的重载

3.1赋值运算符的重载

1、传统写法

· 先进行原有空间的释放和迭代器的置空

· 在复用reserve()和push_back()来进行数据的拷贝

代码部分: 
		vector<T>& operator=(const vector<T>& v){if (*this != v){delete[] _start;_start = _finish = _end_of_storage = nullptr;reserve(v.size());for (auto& x : v){push_back(x);}}return *this;}

2、现代写法

 现代写法就比较简洁,不需要我们来做复杂的工作,,只要实现好了swap()拷贝构造函数就可以实现

swap()的实现
		void swap(vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}
代码部分: 
		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;}

3.2operator []的实现

代码实现:

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

4、迭代器失效问题(重点)

4.1迭代器失效的原因和后果

使用迭代器的主要原因就是为了不暴露容器的底层结构,不需要关心底层结构,让使用更简单

它的底层实际上就是一个指针或对指针进行了封装,比如:vector的迭代器就是原生态指针T* 

因此,迭代器失效就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)

4.2常见迭代器失效的操作

4.2.1扩容相关操作

当 vector 需要扩展容量时,会分配新的内存空间并将原有元素搬移到新的位置。此时,所有的迭代器将会失效,它的本质其实就是野指针

· reserve()

· resize()

· insert()

· push_back()

· assign()

#include <iostream>
using namespace std;
#include <vector>
int main()
{vector<int> v{1,2,3,4,5,6};auto it = v.begin();// 将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容// v.resize(100, 8);// reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容量改变// v.reserve(100);// 插入元素期间,可能会引起扩容,而导致原空间被释放// v.insert(v.begin(), 0);// v.push_back(8);// 给vector重新赋值,可能会引起底层容量改变v.assign(100, 8);/*出错原因:以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。*/while(it != v.end()){cout<< *it << " " ;++it;}cout<<endl;return 0;
}    //  程序运行会崩掉

因此,在进行了扩容后默认迭代器就失效了 

4.2.2删除相关操作

删除操作会使指向被删除元素及其后续元素的迭代器失效。

#include<iostream>
using namespace std;
#include <vector>
int main()
{int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找3所在位置的iteratorvector<int>::iterator pos = find(v.begin(), v.end(), 3);// 删除pos位置的数据,导致pos迭代器失效。v.erase(pos);cout << *pos << endl; // 此处会导致非法访问return 0;
}

erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了

int main()
{xxx::vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);for (const auto& e : v1){cout << e << " ";}cout << endl;//  迭代器失效  //删除所有的偶数:在不同系统下实现结果各有所不同/*auto it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){v1.erase(it);}++it;}*///删除所有的偶数/*auto it = v1.begin();//这种方法在linux下可以进行,但在VS下无法实现//vs会强制检查while (it != v1.end()){if (*it % 2 == 0){v1.erase(it);}else{++it;}}*/               
//  前面两种删除方式都会使迭代器失效  it已经不是指向该指向的位置//该方法适用于多平台	// 迭代器完全体  auto it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){it = v1.erase(it);  // 删除之后重新赋值迭代器,这也是为什么earse要返回迭代器的原因}else{++it;}}for (const auto& e : v1){cout << e << " ";}cout << endl;return 0;
}

总结:

在进行了扩容或删除操作后,参与其中的迭代器就默认失效了,解决办法就是对迭代器进行更新

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

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

相关文章

Java实习生面试题(2025.3.23 be)

一、v-if与v-show的区别 v-show 和 v-if 都是 Vue 中的条件渲染指令&#xff0c;它们的主要区别在于渲染策略&#xff1a;v-if 会根据条件决定是否编译元素&#xff0c;而 v-show 则始终编译元素&#xff0c;只是通过改变 CSS 的 display 属性来控制显示与隐藏。 二、mybatis-…

stm32标准库开发需要的基本文件结构

使用STM32标准库&#xff08;STM32 Standard Peripheral Library&#xff0c;SPL&#xff09;开发时&#xff0c;项目中必须包含一些必要的文件&#xff0c;这些文件确保项目能够正常运行并与MCU硬件交互。以下详细说明&#xff1a; 一、标准库核心文件夹说明 使用标准库开发S…

学生管理系统(需求文档)

需求&#xff1a; 采取控制台的方式去书写学生管理系统 分析&#xff1a; 初始菜单&#xff1a; “----------欢迎来到java学生管理系统----------” “1:添加学生” “2&#xff1a;删除学生” “3&#xff1a;修改学生” “4&#xff1a;查询学生” “5&#xff1a;…

Java算法OJ(13)双指针

目录 1.前言 2.正文 2.1快乐数 2.2盛最多水的容器 2.3有效的三角形的个数 2.4和为s的两个数 2.5三数之和 2.6四数之和 3.小结 1.前言 哈喽大家好吖&#xff0c;今天继续加练算法题目&#xff0c;一共六道双指针&#xff0c;希望能对大家有所帮助&#xff0c;废话不多…

SpringBoot分布式定时任务实战:告别重复执行的烦恼

场景再现&#xff1a;你刚部署完基于SpringBoot的集群服务&#xff0c;凌晨3点突然收到监控告警——优惠券发放量超出预算两倍&#xff01;检查日志发现&#xff0c;两个节点同时执行了定时任务。这种分布式环境下的定时任务难题&#xff0c;该如何彻底解决&#xff1f; 本文将…

MySQL 设置允许远程连接完整指南:安全与效率并重

一、为什么需要远程连接MySQL&#xff1f; 在分布式系统架构中&#xff0c;应用程序与数据库往往部署在不同服务器。例如&#xff1a; Web服务器&#xff08;如NginxPHP&#xff09;需要连接独立的MySQL数据库数据分析师通过BI工具直连生产库多服务器集群间的数据同步 但直接…

系统架构书单推荐(一)领域驱动设计与面向对象

本文主要是个人在学习过程中所涉猎的一些经典书籍&#xff0c;有些已经阅读完&#xff0c;有些还在阅读中。于我而言&#xff0c;希望追求软件系统设计相关的原则、方法、思想、本质的东西&#xff0c;并希望通过不断的学习、实践和积累&#xff0c;提升自身的知识和认知。希望…

动态规划-01背包

兜兜转转了半天&#xff0c;发现还是Carl写的好。 看过动态规划-基础的读者&#xff0c;大概都清楚。 动态规划是将大问题&#xff0c;分解成子问题。并将子问题的解储存下来&#xff0c;避免重复计算。 而背包问题&#xff0c;就是动态规划延申出来的一个大类。 而01背包&…

使用VS2022编译CEF

前提 选择编译的版本 CEF自动编译&#xff0c;在这里可以看到最新的稳定版和Beta版。 从这里得出&#xff0c;最新的稳定版是134.0.6998.118&#xff0c;对应的cef branch是6998。通过这个信息可以在Build requirements查到相关的软件配置信息。 这里主要看Windows下的编译要…

C++20:玩转 string 的 starts_with 和 ends_with

文章目录 一、背景与动机二、string::starts_with 和 string::ends_with&#xff08;一&#xff09;语法与功能&#xff08;二&#xff09;使用示例1\. 判断字符串开头2\. 判断字符串结尾 &#xff08;三&#xff09;优势 三、string_view::starts_with 和 string_view::ends_w…

智能飞鸟监测 守护高压线安全

飞鸟检测新纪元&#xff1a;视觉分析技术的革新应用 在现代化社会中&#xff0c;飞鸟检测成为了多个领域不可忽视的重要环节。无论是高压线下的安全监测、工厂内的生产秩序维护&#xff0c;还是农业区的作物保护&#xff0c;飞鸟检测都扮演着至关重要的角色。传统的人工检测方…

ADC噪声全面分析 -04- 有效噪声带宽简介

为什么要了解ENBW&#xff1f; 了解模数转换器 (ADC) 噪声可能具有挑战性&#xff0c;即使对于最有经验的模拟设计人员也是如此。 Delta-sigma ADC 具有量化和热噪声的组合&#xff0c;这取决于 ADC 的分辨率、参考电压和输出数据速率 (ODR)。 在系统级别&#xff0c;额外的信…

STM32单片机uCOS-Ⅲ系统10 内存管理

目录 一、内存管理的基本概念 二、内存管理的运作机制 三、内存管理的应用场景 四、内存管理函数接口讲解 1、内存池创建函数 OSMemCreate() 2、内存申请函数 OSMemGet() 3、内存释放函数 OSMemPut() 五、实现 一、内存管理的基本概念 在计算系统中&#xff0c;变量、中…

考研课程安排(自用)

文章目录 408数据结构&#xff08;王道&#xff09;计算机组成原理&#xff08;王道&#xff09;操作系统&#xff08;王道&#xff09;计算机网络&#xff08;湖科大版&#xff09; 数学一高等数学&#xff08;微积分&#xff09;线性代数和概率论 408 数据结构&#xff08;王…

ultraiso制作u盘启动

UltraISO制作U盘启动盘的方法 UltraISO是一款功能强大的工具&#xff0c;可以帮助用户将ISO镜像文件写入U盘&#xff0c;从而制作成可启动的系统安装盘。以下是详细的步骤和注意事项&#xff1a; 1. ‌准备工作‌ ‌硬件准备‌&#xff1a;一个容量至少为8GB的U盘&#xff0…

C语言-发布订阅模式详解与实践

文章目录 C语言发布订阅模式详解与实践1. 什么是发布订阅模式&#xff1f;2. 为什么需要发布订阅模式&#xff1f;3. 实际应用场景4. 代码实现4.1 UML 关系图4.2 头文件 (pubsub.h)4.3 实现文件 (pubsub.c)4.4 使用示例 (main.c) 5. 代码分析5.1 关键设计点5.2 实现特点 6. 编译…

蓝桥杯2023年第十四届省赛真题-异或和之差

题目来自DOTCPP&#xff1a; 思路&#xff1a; 什么是异或和&#xff1f; ①题目要求我们选择两个不相交的子段&#xff0c;我们可以枚举一个分界线i&#xff0c;子段1在 i 的左边&#xff0c; 子段2在 i 的右边&#xff0c;分别找到子段1和子段2的最大值、最小值。 ②怎么确…

Linux作业2——有关文件系统权限的练习

1、创建/www目录&#xff0c;在/www目录下新建name和https目录&#xff0c;在name和https目录下分别创建一个index.html文件&#xff0c;name下面的index.html文件中包含当前主机的主机名&#xff0c;https目录下的index.html文件中包含当前主机的ip地址。 #创建/www目录&…

leeCode 70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1. 1 阶 1 阶 2. 2 阶 示例 2&#x…

算法题(105):小猫爬山

审题&#xff1a; 本题需要我们找出将n个小猫放在有限重的缆车上运下山所需的最小缆车数 时间复杂度分析&#xff1a;本题的数据量小于等于18&#xff0c;所以我们在做好剪枝的前提下可以使用深度优先搜索解题 思路&#xff1a; 方法一&#xff1a;dfs 搜索策略&#xff1a;将小…