当前位置: 首页 > news >正文

39 vector深入理解 · 迭代器失效深度浅拷贝

目录

一、迭代器失效

(一)外部迭代器失效

1、扩容引起的野指针问题

2、删除引起的逻辑问题

二、深度浅拷贝


一、迭代器失效

        迭代器可以理解为像指针一样的类对象,但不要一味地认为迭代器就是指针,指针可以实现迭代器,但迭代器也可能是自定义类型,可以通过以下函数调用查看具体类型:

typeid(类型或者对象).name()typeid(vector<int>::iterator).name()

(一)外部迭代器失效

        上一节的末尾介绍的是一种内部迭代器失效,也有外部迭代器失效:

1、扩容引起的野指针问题

        如下代码演示:

void test_vector01()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);for (auto& e : v1){cout << e << " ";}// 打印 1 2 3 4cout << endl;int x;cin >> x;auto it = find(v1.begin(),v1.end(),x);if (it != v1.end()){v1.insert(it, 10 * x);cout << *it << endl;}                
}

        以上代码的功能是在x位置前插入10*x这个数。但在最后的 cout << *it << endl 处会发生迭代器失效的问题:因为 insert函数 是用形参接收实参,形参的改变不影响实参,insert函数虽然修正了形参pos的位置,但实参 it 的位置没有改变,依旧指向被释放的旧空间,是野指针,解引用野指针就会出现报错。

        将insert函数的迭代器参数修改为iterator& pos是不是就可以了?

        答:不行。因为若【迭代器运算的结果】做实参,如:it+2 传过去,这个it+2的结果会保留在一个临时的变量中,而临时变量具有常性,即不可修改(被const修饰),iterator&的话就会造成权限放大的问题。所以不能这样访问,且迭代器只有扩容的情况下才会失效,但我们又不知道什么时候扩容,所以默认迭代器失效了就不能访问

        解决:insert的返回值为【更新后的pos位置】,需要 insert 的话要用 it 接收新的 it 位置。insert代码修改如下:

iterator insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage)//扩容{size_t len = pos - _start;//扩容前计算相对位置reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;//扩容后得出新的pos的位置,这样就不会出现迭代器失效的问题}iterator i = _finish - 1;while (i >= pos){*(i + 1) = *i;i--;}*pos = x;_finish++;return pos;
}

        将演示代码中插入部分的代码修改为:

if (it != v1.end()){it = v1.insert(it, 10 * x);cout << *it << endl;}   

        即可正常访问 it 迭代器的数据。

        总结:外部迭代器第一种失效的原因为扩容引起的野指针的问题

2、删除引起的逻辑问题

        演示代码一:简单删除指定位置数据

void test_vector01()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);for (auto& e : v1){cout << e << " ";}cout << endl;int x;cin >> x;auto it = find(v1.begin(),v1.end(),x);if (it != v1.end()){v1.erase(it);cout << *it << endl;}                
}

        运行后vs进行强制检查后报错,迭代器失效后不让访问 *it。这就是删除数据后,导致数据挪动,it 已经不是指向原来的位置了,会导致逻辑问题。

        下面演示代码更能体现:

void test_vector01()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);for (auto& e : v1){cout << e << " ";}cout << endl;//删除所有的偶数auto it = v1.begin();while(it != v1.end()){if(*it % 2 == 0)v1.erase(it);  ++it;                                                                        }for(auto& e : v1){cout << e << " ";     }                    
}

        因为代码在vs中运行会报错,以下结果在g++中运行得出:

        若数据为:1 2 3 4 5 ,删除后结果为1 3 5;

        但数据为:1 2 2 3 4 5,删除后的结果为1 2 3 5,代码的逻辑就会出现问题;

        若数据为:1 2 2 3 4 5 2,删除后出现段错误(程序崩溃)。

        通过以上数据可以看出是在进行删除后迭代器进行移动而造成的逻辑错误,那么修为进行删除后不挪动迭代器,而不需要删除则进行++能否解决问题?代码如下:

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

        改进后的代码不能解决vs平台的问题,依旧报错,若有删除到一定有效元素个数后就进行缩容的功能,即异地缩容出现,也会失效。

        所以要访问 it迭代器 指向的对象则必须更新 it 的指向,更新的指向为erase函数返回的删除位置。代码如下:

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

        总结:失效的迭代器任何场景下都不能访问位置,更新后的迭代器才能访问。

        注意:erase函数若不更新返回值,指向当前元素的pos迭代器失效,后面元素会牵扯到移动数据,因此pos指向的需要删除的元素后面的迭代器也全部失效。

二、深度浅拷贝

        区分深拷贝与浅拷贝的概念:在自定义类型中,默认的拷贝构造会完成值拷贝(浅拷贝 ):地址与值完全相等地进行拷贝,那么析构原对象与拷贝对象的话就会对同一块空间析构两次,且修改原对象会影响拷贝对象的内容;此时就需要深拷贝,即原对象与拷贝对象的地址不同但值相等。

        当push_back的参数为string类型且扩容的时候会出问题,问题的核心为深层次的深浅拷贝问题,push_back所涉及的代码如下:

void push_back(const T& x)
{insert(_finish, x);
}iterator insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage)//扩容{size_t len = pos - _start;//扩容前计算相对位置reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;//扩容后得出新的pos的位置,这样就不会出现迭代器失效的问题}iterator i = _finish - 1;while (i >= pos)//向后挪数据,腾出空间进行插入{*(i + 1) = *i;i--;}*pos = x;_finish++;return pos;
}void reserve(size_t n)//扩容
{if (n > capacity()){size_t OldSize = size();T* tmp = new T[n];if (_start){memcpy(tmp, _start, sizeof(T) * OldSize);delete[] _start;}_start = tmp;_finish = _start + OldSize;_endofstorage = _start + n;}
}

        解析:

        进行扩容的三部曲:

                ① 开辟一个目标容量大小的空间;

                ② 把旧空间的值拷贝给新空间;

                ③ 更新对象的属性。

        在进行 ① 的内存图如下:

        在 ② 中进行memcpy的时候,进行的是浅拷贝,会把string对象数组中string对象的指针都原封不动地拷贝过来,与旧空间的sting指针指向的同一堆中地址的空间,当旧空间被释放的时候,新空间的string指针依旧指向旧的被释放的空间,就是野指针问题,就会出现乱码。内存图如下:

        改进:把memcpy改成for循环拷贝进行赋值,这样的话若是内置类型就会直接拷贝,若是自定义类型就会调用它本身的深拷贝,不需要过多考虑。改进后代码如下:

void reserve(size_t n)//扩容
{if (n > capacity()){size_t OldSize = size();//扩容三部曲://① new出新空间T* tmp = new T[n];if (_start){//② 拷贝原空间的数据到新空间//memcpy(tmp, _start, sizeof(T) * OldSize);reserve string对象时会出错for (size_t i = 0; i < OldSize; i++){tmp[i] = _start[i];}//③ 释放旧空间并更改原空间的成员变量的值delete[] _start;}_start = tmp;_finish = _start + OldSize; _endofstorage = _start + n;}
}

 


        以上内容仅供分享,若有错误,请多指正。


http://www.mrgr.cn/news/79527.html

相关文章:

  • WebSocket解读
  • YOLOv5+pyqt5+摄像头在特定条件下进行目标检测并采集原始数据
  • SSM 校园一卡通密钥管理系统 PF 于校园图书借阅管理的安全保障
  • 数据结构——对顶堆
  • 计算机网络之NAT、代理服务、内网穿透、内网打洞
  • 超详细搭建PhpStorm+PhpStudy开发环境
  • 【人工智能基础06】人工神经网络基础(二):1. 权值初始化(权值优化、避免梯度爆炸、消失)、2. 权值衰减(防止过拟合)与 3. 权值共享(卷积核)
  • spring boot验证码
  • Kafka服务器的简单部署以及消息的生产、消费、监控
  • 【CKS最新模拟真题】获取多个集群的上下文名称并保存到指定文件中
  • Unity教程(十九)战斗系统 受击反馈
  • 避大坑!Vue3中reactive丢失响应式的问题
  • 链表OJ题型讲解与总结
  • 力扣每日一题 - 999. 可以被一步捕获的棋子数
  • 默认插槽,具名插槽(v-slot:具名,name=‘ ‘),作用域插槽
  • Ubuntu Linux 图形界面工具管理磁盘分区和文件系统(八)
  • 电子商务人工智能指南 1/6 - 搜索、广告和发现
  • 1-1 C语言链表
  • 报错:Invalid HTTP method: PATCH executing PATCH http://XXX.XXX
  • HarmonyOS(63) ArkUI 自定义占位组件NodeContainer
  • centos部署SkyWalking并在springcloud项目中用法举例
  • openharmony开发资料合集
  • 13.在 Vue 3 中使用OpenLayers加载鹰眼控件示例教程
  • 18.[极客大挑战 2019]BabySQL1
  • 书后习题答案:《Python程序设计基础(第2版)》,电子工业出版社,2020.01
  • SpringBoot自动配置底层核心源码