深入篇【C++】谈vector中的深浅拷贝与迭代器失效问题

news/2024/4/30 7:19:36/文章来源:https://blog.csdn.net/Extreme_wei/article/details/131782643

深入篇【C++】谈vector中的深浅拷贝与迭代器失效问题

  • Ⅰ.深浅拷贝问题
    • 1.内置类型深拷贝
    • 2.自定义类型深拷贝
  • Ⅱ.迭代器失效问题
    • 1.内部迭代器失效
    • 2.外部迭代器失效

Ⅰ.深浅拷贝问题

1.内置类型深拷贝

浅拷贝是什么意思?就是单纯的值拷贝。
浅拷贝的坏处:

①空间会析构两次。
②一个修改会影响另一个。

在这里插入图片描述
根据上一篇vector的模拟实现中需要用到拷贝的有三个函数,一个是拷贝构造,一个是赋值重载,一个是扩容。都需要用深度拷贝。深度拷贝是什么意思呢?
就是开出一块空间,将原空间的数据拷贝过来。有时候还需要将原空间释放掉。
在这里插入图片描述
我们可以看一下手搓的vector中的扩容,就是采用的深度拷贝。

void reserve(size_t n){size_t sz = size();if (n > capacity()){T* temp = new T[n];//首先开空间if (_start != nullptr){//将数据拷贝到temp去memcpy(temp, _start, sizeof(T) * sz);//删除原来空间delete[] _start;}//最后将空间赋值给_start_start = temp;_finish = _start + sz;//这里有一个问题,size()的计算是用_finish -start 而这里的start已经改变,而finish还没有改变//最后计算finish就变成空了,最终的问题在于start改变了,所有在之前要保留一份size()的数据_endstroage = _start + n;}}

在这里插入图片描述
这里利用memcpy函数将数据按照字节的方式将start指向的数据一个一个拷贝到temp指向的空间里。
这个是没有问题的,有问题的是这种情况:当vector<string> 类型的数据扩容时,会有隐藏的深拷贝。
我们知道上面的数据是vector<int>类型的。即内置类型,内置类型使用memcpy按照字节的方式拷贝是没有问题的。
但是不是内置类型而是自定义类型时,利用memcpy拷贝可以吗?

void Test4()
{tao::vector<string> v;v.push_back("11111111111111111111111");v.push_back("22222222222222222222222");v.push_back("33333333333333333333333");v.push_back("44444444444444444444444");//v.push_back("55555555555555555555555");for (auto e : v){cout << e << " ";}
}

在这里插入图片描述
当前数据实际大小并没有超过容量,所以没有扩容, 也没有发生拷贝,所以正常,但一旦我们再插入一个数据,就会扩容,这时原来的空间就会被释放,那么能正常打印吗?在这里插入图片描述

很明显运行错误,为什么呢?
在这里插入图片描述

2.自定义类型深拷贝

memcpy是按照字节拷贝的,拷贝完后的数据内容肯定是一样的,所以_str会指向同一块数据。而当拷贝完后原空间就会被释放掉,则temp里的_str就变成野指针了。
vector是深拷贝,但vector空间上存的对象是string类型的数组,使用memcpy会导致string对象的浅拷贝。
【解决方案】
所以我们期望这个对象能进行深拷贝,就比如这个对象是string类型的,我们希望能调用这个自定义类型的深拷贝。而对于那些深拷贝的自定义类型来说,赋值重载必须是深拷贝的,所以我们可以使用这个自定义类型的赋值重载来完成深拷贝。

//扩容------>
void reserve(size_t n){size_t sz = size();if (n > capacity()){T* temp = new T[n];//首先开空间if (_start != nullptr){//将数据拷贝到temp去//memcpy(temp, _start, sizeof(T) * sz);for (size_t i = 0; i < sz; i++){temp[i] = _start[i];//比如_start[i] 是string类型的数据,那么这里赋值给temp就会调用赋值运算符重载,而string的赋值运算符重载是深度拷贝的。}//删除原来空间delete[] _start;}//最后将空间赋值给_start_start = temp;_finish = _start + sz;//这里有一个问题,size()的计算是用_finish -start 而这里的start已经改变,而finish还没有改变//最后计算finish就变成空了,最终的问题在于start改变了,所有在之前要保留一份size()的数据_endstroage = _start + n;}}

在这里插入图片描述
这样原空间释放了,并不会影响temp空间里的数据。这就是隐藏的深拷贝,对于vector<T>呢,如果T是内置类型,使用memcpy就可以解决深浅拷贝,但对于T是自定义类型,memcpy就无法完成深拷贝了,需要使用到T类型的赋值重载来深度拷贝。不过这种方法对于内置类型也是可以完成任务的。
所以我们应该将有拷贝的地方都换成上面的写法,而不是用memcpy来完成拷贝。

//拷贝构造------->vector(const vector<T>& v)//深拷贝: _start(nullptr), _finish(nullptr), _endstroage(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();_endstroage = _start+v.capacity();}

Ⅱ.迭代器失效问题

1.内部迭代器失效

迭代器在遍历访问的时候非常好用,但有的情况下迭代器会发生失效。
什么情况下迭代器会发生失效呢?

①会引起底层空间改变的操作,都有可能引起迭代器失效,比如insert或者push_back插入一个数据时,发生扩容。
②erase删除一个数据时,迭代器发生失效。

什么叫迭代器失效呢?就是不能再访问这个迭代器了,正常使用这个迭代器了。
vector和string都有insert和erase,为什么string没有这个问题呢?因为string中的insert和erase用的不是迭代器而是下标。
在这里插入图片描述
比如insert(iteraort pos,const T&val)在pos位置插入数据时,首先会检查是否需要扩容,发现需要扩容时,会开出2倍空间,将原数据拷贝下来,然后再将原空间释放,最后将temp空间赋给start。
这时就要注意到pos迭代器就已经失效了,为什么?因为pos迭代器原先指向的空间被释放了,现在的pos迭代器就类似一个野指针,危险的很,不能再去使用了。那这时insert插入的pos位置就是一个未知的空间了,肯定会报错的。
【解决方法】
这里pos因为原空间被释放了,变成野指针了,扩容完后的start指向的空间正常,但再去往pos位置去插入数据这就危险了。问题就在于pos位置,我们应该在扩容直接记录一下pos的相对位置,然后扩容后,再更新一下pos的新位置,这样pos迭代器就不会失效了。

iterator insert(iterator pos,const T &val){assert(pos >= _start && pos <= _finish);//首先考虑扩容----这里有一个问题:迭代器失效//当迭代器扩容时,这里的pos迭代器就相当于失效了,因为原来的空间被释放了,pos也就变成野指针了。//需要将将pos迭代器恢复,需要更新pos的新位置。if (_finish == _endstroage){
------------->  size_t len = pos - _start;size_t newcapacity = (capacity() == 0 ? 4 : capacity() * 2);reserve(newcapacity);pos = _start + len;}//使用迭代器的好处就是可以避免string那样头插时,挪动数据,下标要小于0的问题,因为迭代器是一个地址,不可以为0的iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;end--;}*pos = val;_finish++;//insert 中的扩容迭代器失效,外部迭代器的解决方法是使用返回值,将pos位置返回过去,再用迭代器接收,就可以对pos位置上的内容再访问了return pos;//指向新插入位置的迭代器}

2.外部迭代器失效

v里面已经有了4个数据1,2,3,4这时候再在第三个位置插入一个800。
然后再对这个插入位置上的数据修改一下,给这个数据加上1000。
这里的迭代器失效的原因还是因为原空间被销毁,pos位置变成野指针了。要注意虽然我们已经完善了内部迭代器失效,但因为这里是传值传参,形参的改变不会影响实参,虽然形参的迭代器不会失效,但是实参还是会失效的。
所以这里再次访问这个it指向的空间时,就是非法的了,因为it指向的空间被释放了。

tao::vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);tao::vector<int>::iterator it = v.begin()+3;v.insert(it, 800);//这里的it--->pos 形参的改变不影响实参//这里的it迭代器失效了,访问的不是第三个位置了。*it += 1000;for (auto e : v){cout << e << " ";}cout << endl;

在这里插入图片描述
而标准库里的insert也会遇到这样的问题,那么库里的insert是如何解决这个问题的呢?
库里是用返回值的方法来解决的,也就的将插入数据的位置返回回去,用it来接收,那么虽然形参改变无法改变实参,但最后将改变后的形参以返回值的方式再传给实参,实参就有效了。
在这里插入图片描述

tao::vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);tao::vector<int>::iterator it = v.begin()+3;it=v.insert(it, 800);//用返回值的方式将有效的迭代器(指向插入元素的位置)传送回来,那样it就可以使用访问了。*it += 1000;for (auto e : v){cout << e << " ";}cout << endl;

在这里插入图片描述

以上就是insert插入操作引起的迭代器失效问题。接下来我将介绍因为erase删除操作而引起的迭代器问题。
erase操作为什么会引起迭代器失效呢?

1.这里的问题不是类似于空间销毁,变成野指针了,而是这个迭代器代表的位置的意义改变了,指向的内容不一样了。删除pos位置上的元素,pos位置之后的元素就会往前挪动覆盖,pos位置的元素就变成了一个新的元素了。在VS下这个行为就认定为迭代器失效了。
2.即erase以后,迭代器就失效,不能再访问,vs会进行强制检查,如果访问会直接报错。

以下面这个代码为例子:删除vector中的所有偶数

tao::vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(4);v.push_back(4);v.push_back(4);v.push_back(6);for (auto e : v){cout << e << " ";}cout << endl;tao::vector<int>::iterator it = v.begin();while (it != v.end()){if ((*it) % 2 == 0){v.erase(it);//这里erase完,it迭代器就失效了,就无法再访问其内容了,比如连续2个的偶数,第一个被删除后,it位置就变成第二偶数了,但这个位置已经失效了,所以这个偶数就无法正常删除掉了。//1  2  2  3  第一个2可以删除,第二个2无法删除掉。		 }else{++it;//迭代器已经失效了,不能再对这个迭代器操作了。}}for (auto e : v){cout << e << " ";}cout << endl;
}

而正常做法应该是,每次删除完这个位置上的元素后,都要重新赋值更新一下it,库里的做法就是将erase删除位置的下一个元素的位置返回回去,这样it就会更新成被删除元素的的下一个位置。

tao::vector<int>::iterator it = v.begin();while (it != v.end()){if ((*it) % 2 == 0){it=v.erase(it);//正常使用:在每次删除完后,对迭代器重新赋值即可。	 }else{++it;}}

Linux和vs下对于erase删除元素后迭代器是否失效是不同的,VS下对于迭代器的失效检测非常严格,非常极端,而g++的编译器对于迭代器检测就不是很严格了。

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

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

相关文章

react和vue2/3父子组件的双向绑定

目录 Vue .sync&#xff08;2.3.0&#xff09; $emit &#xff08;2.3后&#xff09; 自定义组件的 v-model 2.2.0 v-modelemits(3.0取消了.sync) React 父组件回调函数 相关基础 框架 MVC &#xff08;Model View Controller&#xff09;/MVP&#xff08;Model View…

在云计算环境中,保护Java应用程序可用的有效措施和工具

云计算&#xff08;Cloud&#xff09;技术是近年来计算机科学的一个重要突破。大多数组织已经通过将自己的应用程序移入云平台而获益。不过&#xff0c;如何保证应用程序在第三方服务器上的安全性&#xff0c;是一项艰巨的挑战。 在本文中&#xff0c;我们将重点讨论Java&…

Python_pymysql_与mysql交互

目录 基础功能 简单封装 源码等资料获取方法 基础功能 import pymysql from pymysql.cursors import DictCursor # 导入字典类型的游标对象# 连接数据库 db pymysql.connect(host192.168.3.109, # 数据库IP地址port3306, # 数据库端口号userroot, …

如何解决PDF文件过大无法发送的问题?这几个方法轻松完成PDF压缩~

在我们日常生活中PDF文件已经成为了一种非常常见的文档格式&#xff0c;它具有跨平台和易于阅读的特点&#xff0c;因此很多人都喜欢使用它。然而&#xff0c;当我们需要发送一个较大的PDF文件时&#xff0c;很可能会遇到文件过大而无法发送的问题。那么&#xff0c;该怎样压缩…

基于Dubbo分布式网上售票系统

一、项目介绍 民航售票是一个高度依赖信息业的行业。但在机票销售的管理和规范这方面上存在着很多各种各样的问题。例如订票是客运行业中的一个最基本的业务,表面上看,它只是机票站业务的一个简单的部分,但是它涉及到管理与客户服务等多方面,关系到民航公司能否正常运作。…

学习系统编程No.31【多线程互斥与同步】

引言&#xff1a; 北京时间&#xff1a;2023/7/16/14:32&#xff0c;摆烂至今&#xff0c;在耍这方面&#xff0c;谁能比我行&#xff0c;哈哈哈&#xff0c;乐观&#xff01;欠了一堆课要补&#xff0c;等我们把线程相关知识学完&#xff0c;对于系统编程方面我们搞定的就差不…

ShardingSphere分库分表实战之水平分表

&#x1f680; ShardingSphere &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&…

【动态内存错误详解和C的内存分区】

常见的动态内存错误 1.动态内存错误2.经典案例分析2.1案例一2.1.1**问题分析**2.1.2**修改错误** 2.2案例二2.2.1 原因分析2.2.2 解决问题 c/c内存分布1.2 内存分区简介1.2.1 栈区(stack)1.2.2 堆区(heap)1.2.3 全局(静态)区1.2.4 常量区1.2.5 代码区 1.动态内存错误 &#xf…

面试中关于自动化测试的认识

目录 一、什么是自动化测试&#xff0c;自动化测试的优势是什么&#xff1f; 二、什么样的项目比较适合做自动化测试&#xff0c;什么样的不适合做自动化测试&#xff1f; 三、在制定自动化测试计划的时候一般要考虑哪些点&#xff1f; 四、编写自动化脚本时的一些规范&…

【java】JMeter进行web测试

JMeter进行web测试 1.对网页进行负载测试新建线程组添加默认 HTTP 请求属性添加cookie支持添加HTTP请求添加监听器以便于查看结果登录网站 2. 测试本地web项目3. 其他使用 URL 重写处理用户会话使用标题管理器 参考JMeter用户手册 https://jmeter.net/usermanual/build-web-te…

【C++初阶】list的模拟实现 附源码

一.list介绍 list底层是一个双向带头循环链表&#xff0c;这个我们以前用C语言模拟实现过&#xff0c;->双向带头循环链表 下面是list的文档介绍&#xff1a; list文档介绍 我们会根据 list 的文档来模拟实现 list 的增删查改及其它接口。 二.list模拟实现思路 既然是用C模拟…

C语言项目小游戏之俄罗斯方块

今天给大家带来一个用C语言实现的俄罗斯方块小游戏 游戏截图&#xff1a; 首先我们先创建一个名为mywindows.h的头文件。用来设置我们操作台的各种功能实现 mywindows.h #ifndef MYWINDOWS_H_INCLUDED #define MYWINDOWS_H_INCLUDED//系统调用模块 #include <windows.h&g…

【C语言】指针数组测试题(1万字长文)

江南可采莲&#xff0c;莲叶何田田。鱼戏莲叶间。鱼戏莲叶东&#xff0c;鱼戏莲叶西&#xff0c;鱼戏莲叶南&#xff0c;鱼戏莲叶北。 — 两汉汉乐府《江南》 这篇博客我们将会讲解一些习题&#xff0c;习题是有关于数组和指针的&#xff0c;数组方面的习题也能帮助我们更好的理…

mysql数字开头字符串排序

表结构 CREATE TABLE building (id bigint NOT NULL,name varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT 名称,full_name varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT 全称,PRIMARY KEY (id) USIN…

Redis 最佳实践:7 个维度 + 43 条使用规范,带你彻底玩转 Redis | 附实践清单

目录​​​​​​​ 前言 如何使用 Redis 更节省内存&#xff1f; 1) 控制 key 的长度 2) 避免存储 bigkey 3) 选择合适的数据类型 4) 把 Redis 当作缓存使用 5) 实例设置 maxmemory 淘汰策略 6) 数据压缩后写入 Redis 如何持续发挥 Redis 的高性能&#xff1f; 1) …

HDFS与MapResource笔记

客户端向NN请求上传文件 NN回应可以上传 请求上传块,返回DN 所以后面就比较慢 找最近的服务器进行 64K发到1节点,1节点立刻发给2节点,同时1节点自动开始落盘,这里,3个节点是同时落盘的. 因为缓存是在内存中,而持久化是将数据存到磁盘上. 副本节点选择: 1.安全:放不同机架 2.速…

【实战总结】SpringMVC架构升级SpringCloudAlibaba

升级目标 SpringMVCDubboZookeeper分布式架构改为Spring Cloud Alibaba微服务 技术框架:Spring Boot 2.7.2、Spring Cloud 2021.0.3 & Alibaba 2021.0.1.0 容器:Tomcat 9.0.65 JDK:1.8 配置中心:Nacos 2.0.4 消息队列:RocetMQ 4.9.3 配置中心:Apollo 11.0 缓存: Redis 4.0…

mmdet3d预处理(下)| train pipeline

mmdet3d预处理&#xff08;下&#xff09;—— train pipeline 文章目录 mmdet3d预处理&#xff08;下&#xff09;—— train pipeline基类 BaseTransformLoadPointsFromFileLoadAnnotations3D标签信息&#xff1a;源码 ObjectSample源码 ObjectNoise输入参数源码RandomFlip3D…

Loadrunner结合Fiddler实现脚本的录制

Loadrunner一直被业内认为是最好用的性能测试工具&#xff0c;行业大哥大, 但是用过Loadrunner的朋友都知道&#xff0c;工具功能的确牛&#xff0c;但实际使用过程中总会有一些困扰新手的问题&#xff0c;无法录制脚本&#xff0c; 如遇到Loadrunner不支持的IE版本、对Chrome、…

2023年 大二,我拿到了 3 家大厂 offer,为什么我要安利你去实习?

关于 2023年 大二&#xff0c;我拿到了 3 家大厂 offer 这件事 2023年&#xff0c;在大二那年寒假的时候&#xff0c;提前自学完&#xff0c;觉得自己知识储备差不多了&#xff0c;开始投递软件开发实习&#xff0c;刚开始的时候真的是屡遭打击&#xff0c;首先因为本身是双非二…