【C++/STL】: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. 用迭代器区间初始化

(1) 一个类模版的成员函数可以写成函数模版

(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);

(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根本不是一个区间,编译时就报错了,故需要增加该构造方法。

(2) T()是什么

T()是缺省值,注意这里不能给0,因为T可能为自定义类型,当T为内置类型的时候,也ok。因为C++对内置类型进行了升级,也有构造,为了兼容模版。

5. initializer_list 的构造

(1) C++11新增的类模板initializer_list,方便用花括号初始化

(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的插入一般由下标控制,虽然也重载了迭代器版本,但是并不常用

综上所述,迭代器失效解决办法:在使用前,对迭代器重新赋值即可

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

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

相关文章

SpringBoot整合Flink CDC实时同步postgresql变更数据,基于WAL日志

SpringBoot整合Flink CDC实时同步postgresql变更数据&#xff0c;基于WAL日志 一、前言二、技术介绍&#xff08;Flink CDC&#xff09;1、Flink CDC2、Postgres CDC 三、准备工作四、代码示例五、总结 一、前言 在工作中经常会遇到要实时获取数据库&#xff08;postgresql、m…

为何重视文件加密?用哪款加密软件好呢?

一、公司都重视文件加密的原因有哪些&#xff1f;保护数据安全&#xff1a;在数字化时代&#xff0c;数据是企业重要的资产之一。文件加密可以确保数据在存储和传输过程中不被未经授权的人员访问或窃取&#xff0c;从而保护数据的机密性和完整性。这对于包含敏感信息&#xff0…

Reat hook开源库推荐

Channelwill Hooks 安装 npm i channelwill/hooks # or yarn add channelwill/hooks # or pnpm add channelwill/hooksAPI 文档 工具 Hooks useArrayComparison: 比较两个数组的变化。useCommunication: 处理组件之间的通信。useCurrencyConverter: 货币转换工具。useCurre…

【Docomo】5G

我们想向您介绍第五代移动通信系统“5G”。 5G 什么是5G&#xff1f;支持5G的技术什么是 5G SA&#xff08;独立&#xff09;&#xff1f;实现高速率、大容量的5G新频段Docomo的“瞬时5G”使用三个宽广的新频段 什么是5G&#xff1f; 5G&#xff08;第五代移动通信系统&#x…

【Elasticsearch】Elasticsearch的分片和副本机制

文章目录 &#x1f4d1;前言一、分片&#xff08;Shard&#xff09;1.1 分片的定义1.2 分片的重要性1.3 分片的类型1.4 分片的分配 二、副本&#xff08;Replica&#xff09;2.1 副本的定义2.2 副本的重要性2.3 副本的分配 三、分片和副本的机制3.1 分片的创建和分配3.2 数据写…

Github Benefits 学生认证/学生包 新版申请指南

本教程适用于2024年之后的Github学生认证申请&#xff0c;因为现在的认证流程改变了很多&#xff0c;所以重新进行了总结这方面的指南。 目录 验证教育邮箱修改个人资料制作认证文件图片转换Base64提交验证 验证教育邮箱 进入Email settings&#xff0c;找到Add email address…

【一图学技术】5.OSI模型和TCP/IP模型关系图解及应用场景

OSI模型和TCP/IP模型关系图解 OSI模型和TCP/IP模型都是网络通信的参考模型&#xff0c;用于描述网络协议的层次结构和功能。下面是它们的定义和区别&#xff1a; OSI模型&#xff08;Open Systems Interconnection Model&#xff09; OSI模型是一个理论上的七层模型&#xff…

揭秘线性代数秩的奥秘:从理论到机器学习的跨越

一、线性代数中的秩&#xff1a;定义与性质 1.1 定义 在线性代数中&#xff0c;秩是一个核心概念&#xff0c;用于描述矩阵或向量组的复杂性和独立性。具体而言&#xff0c;一个矩阵的秩定义为该矩阵中非零子式的最高阶数&#xff0c;而一个向量组的秩则是其最大无关组所含的…

双 Token 三验证解决方案

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 问题分析 以往的项目大部分解决方案为单 token&#xff1a; 用户登录后&#xff0c;服务端颁发 jwt 令牌作为 token 返回每次请求&#xff0c;前端携带 token 访问&#xff0c;服务端解析 token 进行校验和…

Ubuntu配置项目环境

目录 一、Xshell连接云服务器 二、切换到root用户 三、安装jdk 四、安装tomcat 五、安装mysql 1、安装mysql服务器 2、卸载mysql服务器 六、正式进行程序的部署 一、Xshell连接云服务器 要想使用xshell连接上云服务器就需要明确云服务器的几个信息&#xff1a; 1&…

科研绘图系列:R语言GWAS曼哈顿图(Manhattan plot)

介绍 曼哈顿图(Manhattan Plot)是一种常用于展示全基因组关联研究(Genome-Wide Association Study, GWAS)结果的图形。GWAS是一种研究方法,用于识别整个基因组中与特定疾病或性状相关的遗传变异。 特点: 染色体表示:曼哈顿图通常将每个染色体表示为一个水平条,染色体…

tarojs项目启动篇

TaroJS 是一个开放式跨端开发解决方案&#xff0c;使用 React 语法规范来开发多端应用&#xff08;包括小程序、H5、React Native 等&#xff09;。它可以帮助开发者高效地构建出在不同端上运行一致的应用。以下是启动 TaroJS 项目&#xff08;本来就有的旧项目&#xff09;的步…

⭐️2024年7月全球排名前二十开发语言全面对比横向竖向PK(TIOBE指数榜单)编程语言介绍 适用场景 优势 举例 详细说明 编写第一个语言程序Hello world源代码

2024年7月全球排名前二十开发语言全面对比横向竖向PK&#xff08;TIOBE指数榜单&#xff09;编程语言介绍 适用场景 优势 举例 详细说明 编写第一个语言程序Hello world源代码 2024年7月全球排名前二十开发语言全面对比横向竖向PK&#xff08;TIOBE指数榜单&#xff09;编程语言…

反序列化靶机serial

1.创建虚拟机 2.渗透测试过程 探测主机存活&#xff08;目标主机IP地址&#xff09; 使用nmap探测主机存活或者使用Kali里的netdicover进行探测 -PS/-PA/-PU/-PY:这些参数即可以探测主机存活&#xff0c;也可以同时进行端口扫描。&#xff08;例如&#xff1a;-PS&#xff0…

【python】Python中采集Prometheus数据,进行数据分析和可视化展示

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

如何在 Debian 上安装运行极狐GitLab Runner?【二】

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门面向中国程序员和企业提供企业级一体化 DevOps 平台&#xff0c;用来帮助用户实现需求管理、源代码托管、CI/CD、安全合规&#xff0c;而且所有的操作都是在一个平台上进行&#xff0c;省事省心省钱。可以一键安装极狐GitL…

本地生活服务商公司有哪些?一文教你搭建本地生活系统!

当前&#xff0c;本地生活领域群雄环伺&#xff0c;日益激烈的竞争推动各家互联网大厂调整布局模式的同时&#xff0c;也让本地生活市场持续迸发新的活力。在此背景下&#xff0c;想要通过本地生活服务商身份入局的创业者数量不断增多&#xff0c;以本地生活服务商公司有哪些等…

BEVGPT展示自动驾驶的“全知视角”,预测决策规划三合一的革新之作!

前言 本篇文章由原paper一作Pengqin Wang&#xff08;王鹏钦&#xff09;全权翻译分享&#xff0c;王鹏钦为香港科技大学博士生&#xff0c;师从沈劭劼教授、朱美新教授。他的研究方向为自动驾驶和机器人系统中的决策、预测和规划。他的研究成果发表于TMECH、RAL、IROS、TRB等…

互联网政务应用安全管理规定

互联网政务应用安全管理规定 &#xff08;2024年2月19日中央网络安全和信息化委员会办公室、中央机构编制委员会办公室、工业和信息化部、公安部制定 2024年5月15日发布&#xff09; 第一章 总则 第一条为保障互联网政务应用安全&#xff0c;根据《中华人民共和国网络安全法…

【前端新手小白】学习Javascript的【开源好项目】推荐

目录 前言 1 项目介绍 1.1 时间日期类 1.2 网页store类 1.3 事件类 1.4 Number类 1.5 String类 1.6 正则验证类 1.7 ajax类 1.8 data数据类 1.9 browser浏览器类 2 学习js-tool-big-box开源项目时有哪些收获 2.1 你可以这样做 2.2 如果你需要使用本项目 2.3 你…