【C++】:vector容器的底层模拟实现迭代器失效隐藏的浅拷贝
目录
- 💡前言
- 一,构造函数
- 1 . 强制编译器生成默认构造
- 2 . 拷贝构造
- 3. 用迭代器区间初始化
- 4. 用n个val值构造
- 5. initializer_list 的构造
- 二,析构函数
- 三,关于迭代器
- 四,有关数据个数与容量
- 五,交换函数swap
- 六,赋值拷贝
- 七,[ ]运算符
- 八,预留空间(扩容)
- 8.1 使用memcpy拷贝问题(重点)
- 九,尾插和尾删数据
- 十,插入数据 insert
- 十一,删除数据 erase
- 十二,insert和erase的迭代器失效问题(重点)
- 1. 什么是迭代器失效?
- 2. insert 的迭代器失效
- 2.1 insert 内部pos位置的失效
- 2.2 insert 以后外部的实参失效
- 3. erase 以后的迭代器失效
💡前言
上篇文章已经介绍了vector容器的基本使用vector容器的基本使用,这篇文章主要选择vector中一些核心的,基本的接口进行模拟实现。
注意:由于我们模拟实现时使用了类模板,所以不建议进行文件分离,不然会产生链接错误。所以我们把函数都写在.h文件中,在Test.cpp文件中进行测试。
首先我们先给出vector类:
#include <assert.h>
#include <vector>
#include <iostream>
using namespace std;template<class T>
class vector
{
public:// Vector的迭代器是一个原生指针typedef T* iterator;typedef T* const_iterator;//......private:iterator _start = nullptr;//指向开始位置的指针iterator _finish = nullptr;//指向最后一个位置的下一个位置的指针iterator _end_of_storage = nullptr;//指向存储容量的尾
};
一,构造函数
在vector文档中,构造函数分为好几个类型,下面分别进行介绍:
1 . 强制编译器生成默认构造
无参构造
vector() = default;
2 . 拷贝构造
//v2(v1);
vector(const vector<T>& v)
{//提前预开空间,避免边尾插边扩容reserve(v.capacity());for (auto e : v){//this->push_back(e);push_back(e);}
}
3. 用迭代器区间初始化
3.1 一个类模版的成员函数可以写成函数模版。
3.2 若使用iterator做迭代器,会导致初始化的迭代器区间[first,last)只能是vector的迭代器,重新声明迭代器,迭代器区间[first,last)可以是任意容器的迭代器。
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}
4. 用n个val值构造
vector(size_t n, const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}vector(int n, const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}
具体使用:
//初始化3个空
vector<string> v1(3);//初始化为3个xx
vector<string> v2(3, "xx");//初始化为3个1
vector<int> v3(3, 1);//err 非法的间接寻址.参数匹配问题
vector<int> v3(3u, 1);//ok//如果非要这样传参数,就需要再重载一个构造
vector<int> v3(3, 1);
4.1 为什么要重载两个类似的构造?
理论上将,提供了vector(size_t n, const T& value = T())之后vector(int n, const T& value = T())就不需要提供了,但是对于:vector< int > v(10, 5);
编译器在编译时,认为T已经被实例化为int,而10和5编译器会默认其为int类型, 就不会走vector(size_t n, const T& value = T())这个构造方法, 最终选择的是:vector(InputIterator first, InputIterator last)。因为编译器觉得区间构造两个参数类型一致,因此编译器就会InputIterator实例化为int,但是10和5根本不是一个区间,编译时就报错了,故需要增加该构造方法。
4.2 T()是什么?
T()是缺省值,注意这里不能给0,因为T可能为自定义类型,当T为内置类型的时候,也ok。因为C++对内置类型进行了升级,也有构造,为了兼容模版。
5. initializer_list 的构造
5.1. C++11新增的类模板initializer_list,方便初始化。
5.2. 它的内部其实有两个指针,一个指向第一个值的位置,一个指向最后一个值的下一个位置,并且支持迭代器。
vector(initializer_list<T> il)
{reserve(il.size());for (auto e:il){push_back(e);}
}
具体使用:
支持被花括号括起的任意个数的值给 initializer_list
auto il1 = { 1,3,4,5,6,7 };
initializer_list<int> il2 = { 1,2,3 };//这里的隐式类型转换不一样,参数个数不固定
vector<int> v4 = { 7,8,9,4,5 };//隐式类型转换
vector<int> v5({ 4,5,6 });//直接构造
二,析构函数
~vector()
{if (_start){delete[] _start;_start = _finish = _end_of_storage = nullptr;}
}
三,关于迭代器
iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin()const
{return _start;
}const_iterator end()const
{return _finish;
}
四,有关数据个数与容量
//计算有效数据个数
size_t size()const
{return _finish - _start;
}//计算当前容量
size_t capacity()const
{return _end_of_storage - _start;
}
五,交换函数swap
与string类类似,依然调用库里的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);
}
六,赋值拷贝
与string类的现代写法相同。
//赋值拷贝
//v3 = v1;
vector<T>& operator=(const vector<T> v)
{swap(v);return *this;
}
七,[ ]运算符
//[]运算符
T& operator[](size_t pos)
{//断言,避免下标越界assert(pos < size());return _start[pos];
}
八,预留空间(扩容)
void reserve(size_t n)
{//由于开空间后_start指向改变,所以要提前记录原来的有效数据个数//以便在新空间中更新_finish的位置size_t old_size = size();if (n > capacity()){T* tmp = new T[n];//开新空间if (_start){//memcpy(tmp, _start, sizeof(T) * old_size);for (size_t i = 0; i < old_size; i++){//此时这里的赋值调用的是string的赋值,肯定是深拷贝tmp[i] = _start[i];}delete[] _start;//释放旧空间}_start = tmp;//改变指向,指向新空间//在新空间里更新_finish,_end_of_storage的位置_finish = _start + old_size;_end_of_storage = _start + n;}
}
8.1 使用memcpy拷贝问题(重点)
假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?
int main()
{bite::vector<bite::string> v;v.push_back("2222");v.push_back("2222");v.push_back("2222");return 0;
}
问题分析:
(1) memcpy是内存的二进制格式拷贝,按字节拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。
(2) 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
图解如下:
当把原空间里的"2222"拷贝进新空间后,delete会先对原空间的每个对象调用析构函数,再把原空间销毁,但是此时tmp仍指向那块空间,变成野指针了!
复用赋值进行修改后:
此时这里的赋值调用的是string的赋值,肯定是深拷贝
九,尾插和尾删数据
//尾插数据
void push_back(const T& x)
{if (_finish == _end_of_storage){size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);}*_finish = x;++_finish;
}//尾删
void pop_back()
{//断言,确保有数据可删assert(size() > 0);--_finish;
}
十,插入数据 insert
//void insert(iterator pos, const T& x)
iterator insert(iterator pos, const T& x)
{//断言,判断下标的有效性assert(pos >= _start && pos <= _finish);//判断是否扩容if (_finish == _end_of_storage){size_t len = pos - _start;size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);//insert函数的内部迭代器失效问题:类似于野指针问题//扩容后pos位置失效,需要重新计算pospos = _start + len;}//挪动数据再插入iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*(end + 1) = x;++_finish;//返回新插入那个位置的迭代器return pos;
}
十一,删除数据 erase
iterator erase(iterator pos)
{assert(pos >= _start && pos < _finish);iterator end = pos + 1;while (end != _finish){*(end - 1) = *end;++end;}--_finish;//返回删除位置的下一个位置的迭代器return pos;
}
十二,insert和erase的迭代器失效问题(重点)
1. 什么是迭代器失效?
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
2. insert 的迭代器失效
2.1 insert 内部pos位置的失效
当刚好执行了扩容操作这一步时,由于要开辟新空间,拷贝数据,释放空间,改变空间指向。但是此时pos位置还在原空间,这就使pos变成了野指针,如果对该位置进行访问,就会导致程序崩溃。
所以在函数内部,扩容后我们要更新pos迭代器!
2.2 insert 以后外部的实参失效
演示代码如下:
void vector_test2()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);for (size_t i = 0; i < v1.size(); i++){cout << v1[i] << " ";}cout << endl;//输入x,查找是否存在,如果存在,在该位置前插入10int x;cin >> x;vector<int>::iterator it = find(v1.begin(), v1.end(), x);//用返回值接收it = v1.insert(it, 10);//建议失效后的迭代器不要访问,除非使用返回值。cout << *it << endl;for (size_t i = 0; i < v1.size(); i++){cout << v1[i] << " ";}cout << endl;
}
insert以后it这个实参会不会失效呢?答案是:会的。
在扩容的时候一定会导致迭代器失效。因为虽然在insert内部形参修正了,但是形参的改变不影响实参。
迭代器失效的建议是:不要使用失效的迭代器!
如果非要访问此时插入的那个位置,必须使用 insert 的返回值,返回的是新插入那个位置的迭代器。
3. erase 以后的迭代器失效
erase 以后的迭代器失效问题比较复杂,它与平台相关。
代码演示如下:
int main()
{int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找3所在位置的iteratorvector<int>::iterator it = find(v.begin(), v.end(), 3);// 删除it位置的数据,导致it迭代器失效。//v.erase(it); // errit = v.erase(it); // okcout << *it<< endl; // 此处会导致非法访问return 0;
}
erase 以后it这个实参会不会失效呢?答案是:会的。
erase删除pos位置元素后,it位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果it刚好是最后一个元素,删完之后it刚好是end的位置,而end位置是没有元素的,那么it就失效了。或者说某个编译器有这样的机制:当删除到一定的数量时,会进行缩容处理,此时it也就相当于野指针了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
如果非要访问这个位置,也需要用返回值重新赋值!
以下代码的功能是删除vector中所有的偶数:
int main()
{vector<int> v{ 1, 2, 3, 4 };auto it = v.begin();while (it != v.end()){if (*it % 2 == 0)//v.erase(it); // errit = v.erase(it); // ok++it;}return 0;
}
这个代码在VS平台下一定会运行崩溃!因为删除后it位置已经失效了,此时再对it进行访问(++操作或是解引用操作都是)会程序报错。
但是在Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端,所以正常运行。
注意:
- 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效,但是string的插入一般由下标控制,虽然也重载了迭代器版本,但是并不常用。
综上所述,迭代器失效解决办法:在使用前,对迭代器重新赋值即可。
相关文章:

【C++】:vector容器的底层模拟实现迭代器失效隐藏的浅拷贝
目录 💡前言一,构造函数1 . 强制编译器生成默认构造2 . 拷贝构造3. 用迭代器区间初始化4. 用n个val值构造5. initializer_list 的构造 二,析构函数三,关于迭代器四,有关数据个数与容量五,交换函数swap六&am…...

必看项目|多维度揭示心力衰竭患者生存关键因素(生存分析、统计检验、随机森林)
1.项目背景 心力衰竭是一种严重的公共卫生问题,影响着全球数百万人的生活质量和寿命,心力衰竭的病因复杂多样,既有个体生理因素的影响,也受到环境和社会因素的制约,个体的生活方式、饮食结构和医疗状况在很大程度上决定了其心力衰竭的风险。在现代社会,随着生活水平的提…...
centos安装Redis
在CentOS上安装Redis的步骤如下: 使用yum安装依赖库: sudo yum install -y gcc make 下载Redis源码: wget http://download.redis.io/releases/redis-6.0.9.tar.gz 解压Redis源码: tar xzf redis-6.0.9.tar.gz 编译Redis&…...
继承与多态2
2.5(杨.丹尼尔梁英文第11版P537:*13.12)(几何对象的面积求和)写一个方法,将数组中所有几何对象的面积求和。 方法签名是: 公共静态双求和区域(几何对象【】a) 编写一个测试程序&…...
在RT-Thread下为MPU手搓以太网MAC驱动-3
文章目录 MAC驱动支持不同的PHY芯片关于对PHY设备抽象的改进RT-Thread下PHY设备抽象接口的改进关于对PHY设备抽象的改进 这是个人驱动开发过程中做的一些记录,仅代表个人意见和理解,不喜勿喷 MAC驱动需要支持不同的PHY芯片 MAC驱动支持不同的PHY芯片 关…...
Cocos Creator 2D物理引擎的使用详解
前言 Cocos Creator是一款优秀的游戏开发工具,它提供了强大的2D物理引擎,帮助开发者轻松实现游戏中的物理效果。在本文中,我们将详细介绍Cocos Creator中2D物理引擎的使用方法,并通过代码实现来演示其具体应用。 对惹࿰…...

618局外人抖音:别人挤压商家“拼价格”,它默默联合商家“抢用户”?
文|新熔财经 作者|宏一 “618”来临之际,各电商平台和短视频平台早已打响了“促销大战”。不过,今年各大平台都更积极适应新的消费形式,调整了“大促动作”。 比如淘宝、京东带头取消了沿用十年之久的预售机制&…...

【Unity AR开发插件】五、运行示例程序
专栏 本专栏将介绍如何使用这个支持热更的AR开发插件,快速地开发AR应用。 链接: Unity开发AR系列 热更数据制作:制作热更数据-AR图片识别场景 插件简介 通过热更技术实现动态地加载AR场景,简化了AR开发流程,让用户可…...
JavaScript className 类名属性操作
在JavaScript中,可以通过className属性来操作HTML元素的类名。 添加类名:可以使用element.className "className"来添加一个类名到元素中。 var element document.getElementById("myElement"); element.className " newC…...

做场外个股期权怎么询价
做场外个股期权怎么询价?没有具体的哪家做市商是询价是最低的,个人投资者需要通过机构通道方询价进行对比,各券商的报价由询价机构方提供给到投资者,可以参考不同券商的报价进行比对,再决定是否进行投资。本文来自&…...

Databend 开源周报第 146 期
Databend 是一款现代云数仓。专为弹性和高效设计,为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务:https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展,遇到更贴近你心意的 Databend 。 支持 Expressio…...
Android12.0 SIM卡语言自适应
文章目录 需求语言设定Settings中语言切换流程检测到SIM卡,更新系统语言最终修改 需求 要求系统语言跟随SIM卡的语言变化。 语言设定 (1)系统预置语言, 即在makefile中指定的语言 (2)重启, 如果未插卡, 则系统语言为预置的语言 (3)重启插入SIM卡开机, 会自适应为…...

滴滴一季度营收同比增长14.9%至491亿元 经调整EBITA盈利9亿元
【头部财经】5月29日,滴滴在其官网发布2024年一季度业绩报告。一季度滴滴实现总收入491亿元,同比增长14.9%;经调整EBITA(非公认会计准则口径)盈利9亿元。其中,中国出行一季度实现收入445亿元,同…...

C语言 指针——指针变量的定义、初始化及解引用
目录 指针 内存如何编址? 如何对变量进行寻址? 用什么类型的变量来存放变量的地址? 如何显示变量的地址?编辑 使用未初始化的指针会怎样? NULL是什么? 如何访问指针变量指向的存储单元中的数据? 指针变量的…...

详解 Spark 的运行架构
一、核心组件 1. Driver Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作主要负责: 将用户程序转化为作业 (job)在 Executor 之间调度任务 (task)跟踪 Executor 的执行情况通过 UI 展示查询运行情况 2. Exec…...

盲盒小程序开发,为市场带来的新机遇
近年来,盲盒市场一直处于热门行业中,发展非常快速。在互联网的支持下,也衍生出了线上盲盒小程序,实现了线上线下双发展的态势。 盲盒小程序作为一种新的盲盒购物方式,受到了盲盒消费者的喜爱,为盲盒行业的…...

stm32学习-流水灯
接线 注意:LED灯长一点的引脚是正极。 配置GPIO 1.使用RCC开启GPIO时钟 void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState); void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState); void RCC_APB1Perip…...

GIGE 协议摘录
系列文章目录 GIGE 学习笔记 GIGE 协议摘录 文章目录 系列文章目录引言第 1 章 设备发现1.1 链路选择1.1.1 单链路配置1.1.2 多链路配置1.1.3 链路聚合组配置 LAG 1.2 IP配置1.2.1 协议选择1.2.2 静态IP1.2.3 DHCP1.2.4 链接本地地址 LLA 1.3 设备枚举1.3.1 GVCP设备发现广播设…...

服务器的远程桌面无法连接,服务器远程桌面无法连接问题处理教程
服务器的远程桌面无法连接,服务器远程桌面无法连接问题处理教程。 一、问题概述 服务器远程桌面无法连接是日常运维中常见的问题之一。它可能由多种原因造成,如网络问题、服务器配置错误、远程桌面服务未启动等。本教程将指导您逐步排查并解决这些问题。…...

【机器学习300问】105、计算机视觉(CV)领域有哪些子任务?
计算机视觉作为人工智能的重要分支,发展至今已经在诸多领域取得显著的成果。在众多的计算机视觉任务中,图像分类、目标检测与定位、语义分割和实例分割是四个基本而关键的子任务,它们在不同的应用场景下扮演着重要角色。这四个子任务虽然各具…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...

多模态大语言模型arxiv论文略读(108)
CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题:CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者:Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...

基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...