`
canco
  • 浏览: 255942 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

STL序列式容器中删除元素的方法和陷阱 一

阅读更多

STL(标准模板库)中经常会碰到要删除容器中部分元素的情况,本人在编程中就经常编写这方面的代码,在编码和测试过程中发现在STL中删除容器有很多陷阱,网上也有不少网友提到如何在STL中安全删除元素这些问题。本文将讨论编程过程中最经常使用的两个序列式容器vectorlist中安全删除元素的方法和应该注意的问题,       其它如queuestack等配接器容器(container adapter),由于它们有专属的操作行为,没有迭代器(iterator),不能采用本文介绍的删除方法,至于deque,它与vector的删除方法一样。STL容器功能强大,but no siliver bullet,如果你使用不当,也将让你吃尽苦头。

1.手工编写for循环代码删除STL序列式容器中元素的方法<o:p></o:p>

例如,你能看出以下代码有什么问题?

1

#include <iostream>

#include <vector>

using namespace std;

void main( ) {

       vector<int> vectInt;

       int i;

       //     初始化vector容器

       for (i = 0; i < 5; i++ ) {

              vectInt.push_back( i );

       }

       //     以下代码是要删除所有值为4的元素

       vector<int>::iterator itVect = vectInt.begin();

       for ( ; itVect != vectInt.end();  ++itVect ) {

              if ( *itVect == 4 ) {

                     vectInt.erase( itVect );

              }

       }

       int iSize = vectInt.size();

      for (  i = 0 ; i < iSize; i++ )  {

                     cout << " i= " << i <<  ", " << vectInt[ i ] << endl;

       }

 <o:p></o:p>

}

2

#include <iostream>

#include <vector>

using namespace std;

void main( ) {

       vector<int> vectInt;

       int i;

       //     初始化vector容器

       for ( i = 0; i < 5; i++ ) {

              vectInt.push_back( i );

              if ( 3 == i ) {

                     //       使3的元素有两个,并且相临。这非常关键,否则将发现不了bug

                     //   具体解释见下。

                     vectInt.push_back( i );

              }

       }

       vector<int>::iterator itVect = vectInt.begin();

       vector<int>::iterator itVectEnd = vectInt.end(); //       防止for多重计算

       //     以下代码是要删除所有值为3的元素

       for ( ; itVect != itVectEnd; ++itVect ) {

              if ( *itVect == 3 ) {

                     itVect = vectInt.erase( itVect );

              }

       }

       int iSize = vectInt.size();

      for (  i = 0 ; i < iSize; i++ )  {

                     cout << " i= " << i <<  ", " << vectInt[ i ] << endl;

       }

3

#include <iostream>

#include <vector>

using namespace std;

void main( ) {

       vector<int> vectInt( 5 );

       int i;

       vectInt[ 0 ] = 0;

       vectInt[ 1 ] = 1;

       vectInt[ 2 ] = 2;

       vectInt[ 3 ] = 3;

       vectInt[ 4 ] = 4; //     替换为 vectInt[ 4 ] = 3;试试

       vector<int>::iterator itVect = vectInt.begin();

       vector<int>::iterator itVectEnd = vectInt.end(); //       防止for多重计算

       //     以下代码是要删除所有值为3的元素

       for ( ; itVect != itVectEnd; ) {

              if ( *itVect == 3 ) {

                     itVect = vectInt.erase( itVect );

              }

              else {

                     ++itVect;

              }

       }

       int iSize = vectInt.size();

      for (  i = 0 ; i < iSize; i++ )  {

                     cout << " i= " << i <<  ", " << vectInt[ i ] << endl;

       }

}

 <o:p></o:p>

分析:

这里最重要的是要理解erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针,指向容器中的元素,现在删除了这个元素,将导致内存重新分配,相应指向这个元素的迭代器之后的迭代器就失效了,但erase成员函数返回要被删除的itVect之后的迭代器

1将导致程序未定义的错误,在windows中即是访问非法内存,程序当掉。因为vectInt.erase( itVect );调用后itVect之后的迭代器已无效了,所以当执行++itVect后,*itVect访问了非法内存。例1也是初学者最容易犯的错误,这个错误也比较容易发现。

2可能会导致不能把vectInt中所有为3的元素删除掉。因为第一次删除成功时,itVect = vectInt.erase( itVect );itVect为指向3之后的位置,之后再执行++itVectitVect就掉过了被删除元素3之后的元素3,导致只删除了一个为3的元素,这个bug比较隐蔽,因为如果不是两个均为3的元素相临,就将很难捕捉到这个bug,程序有可能在一段时间运行良好,但如碰到容器中两值相同的元素相临,则程序就要出问题。

3,对于本例你可能要说程序没有任何问题,解决了上面的两个bug,程序也运行正常。但且慢,你把 vectInt[ 4 ] = 4; 这一行改为 vectInt[ 4 ] = 3;”试试,一运行,程序当掉,访问非法内存!你疑惑不解:从程序看不出bug,而且我还把vectInt.end()放在外面计算以防止for多重计算,提高效率。哈哈,问题就出在最后一句话!算法大师Donald Knuth有一句名言:不成熟的优化是一切恶果的根源( Permature optimization is the root of all evil )。由于在for循环中要删除元素,则vectInt.end()是会变化的,所以不能在for循环外计算,而是每删除一次都要重新计算,所以应放在for循环内。那你要问,为什么把 vectInt[ 4 ] = 4; 这一行改为 vectInt[ 4 ] = 3;”程序就会当掉,而不改程序就很正常呢?这就跟vector的实现机制有关了。下面以图例详细解释。

vectInt的初始状态为:

 <o:p></o:p>

                                                                             | end

0        1        2        3         4      <o:p></o:p>

                                     

删除3后,

 <o:p></o:p>

                                                                |新的end  | 原来的end

0        1        2        4         4      <o:p></o:p>

 <o:p></o:p>

 <o:p></o:p>

 <o:p></o:p>

注意上面“新的end”指向的内存并没有被清除,为了效率,vector会申请超过需要的内存保存数据,删除数据时也不会把多余的内存删除。

然后itVect再执行++itVect,因为此时*itVect等于4,所以继续循环, 这时itVect 等于“新的end”但不等于“原来的end”(它即为itVectEnd),所以继续,因为 *itVect访问的是只读内存得到的值为4,不等于3,故不删除,然后执行++itVect此时itVect等于itVectEnd退出循环。从上面过程可以看出,程序多循环了一次(删除几次,就要多循环几次),但程序正常运行。

如果把 vectInt[ 4 ] = 4; 这一行改为 vectInt[ 4 ] = 3;”过程如下:<o:p></o:p>

 <o:p></o:p>

 <o:p></o:p>

                                                                        | end

0        1        2        3         3      <o:p></o:p>

 <o:p></o:p>

删除3后,

 <o:p></o:p>

                                                                |新的end       |原来的 end

0        1        2        3         3      <o:p></o:p>

 <o:p></o:p>

 <o:p></o:p>

删除第23后,

 <o:p></o:p>

                                                 |新的end            |原来的 end

0        1        2        3         3      <o:p></o:p>

 <o:p></o:p>

这时itVect 等于“新的end”但不等于“原来的end”(它即为itVectEnd),所以继续,因为 *itVect访问的是只读内存得到的值为3,等于3,所以执行删除,但因为*itVect访问的是只读内存不能删除,所以程序当掉。

综上,我们知道当要删除的值在容器末尾时,会导致程序删除非法内存,程序当掉;即使程序正常运行,也是for循环多执行了等于删除个数的循环。所以把vectInt.end()放在for循环外面执行,完全是错误的。对于list容器,list.end()在删除过程中是不会变的,可以把它放在for循环外面计算,但由于list.end()是个常量,把list.end()放在for循环中计算编译器应该可以优化它。从安全考虑,除非你能保证for循环中不会改变容器的大小,否则都应该对容器的值在for循环中计算,对于 vectInt.size()这样的计算,也应该在for循环中计算,不要因为微小的优化而导致程序出错。

 <o:p></o:p>

正确的方法:

4

#include <iostream>

#include <vector>

using namespace std;

void main( ) {

       vector<int> vectInt;

       int i;

分享到:
评论

相关推荐

    Effectiv STL, STL源码剖析, STL标准程式库,The C++ standard library

    2. **迭代器**:迭代器是STL的核心概念,它是访问容器中元素的一种抽象接口。通过迭代器,程序员可以像操作指针一样遍历容器,但具有更丰富的功能,如前向、双向和随机访问迭代器,支持各种操作如递增、递减、比较和...

    关于STL的erase()陷阱-迭代器失效问题的总结

    对于序列式容器(如`vector`、`deque`),删除操作可能导致后续迭代器失效。 了解这些规则并正确使用迭代器是使用STL时的关键,避免迭代器失效能确保代码的稳定性和安全性。在实际编程中,一定要注意这些细节,以...

    C++函数手册+(LibraryFunctions)

    4. **容器管理**:C++标准模板库(STL)提供了多种容器,如`vector`动态数组,`list`双向链表,`set`和`map`为关联容器,分别提供有序的元素集合和键值对映射。容器相关的函数如`push_back()`向`vector`末尾添加元素...

    TSP-issues.rar_数值算法/人工智能_Visual_C++_

    - 可能会用到STL库中的容器,如vector,用于表示种群和个体。 4. **优化策略**: - 可以使用各种策略改进遗传算法,例如 elitism(保留最优解)、dithering(适应度阈值)和局部搜索。 - 还可以引入多种交叉和...

    cole_02_0507.pdf

    cole_02_0507

    工程硕士开题报告:无线传感器网络路由技术及能量优化LEACH协议研究

    内容概要:南京邮电大学工程硕士研究的无线传感器网络路由技术。通过对无线传感器网络路由协议的历史和研究现状进行了详细探讨,着重介绍了SPIN、LEACH、TEEN、pEGASIS等常见协议的特点、优势与局限性。文中分析了现有路由协议中的能量管理和网络覆盖问题,并提出了一种结合最大覆盖模型的改进型能量LEACH协议来应对这些问题。该研究旨在提高无线传感网络能量效率和覆盖效果,从而拓展其在各行业尤其是环境监测和军事安全领域的大规模应用。 适合人群:本篇文章主要面向具有无线传感网路研究背景或对此有兴趣的研究人员、工程师和技术爱好者,特别是在能源消耗控制上有较高需求的应用开发者。 使用场景及目标:①帮助理解和选择合适的无线传感器网络路由技术;②指导开发新路由协议时关注的关键要素;③为企业实施物联网相关项目提供理论支撑。 其他说明:文章强调了优化算法对于改善系统性能的重要性,并展示了具体的实施方案。通过仿真实验对不同协议的效果进行了验证,体现了科学研究的严谨态度与实践导向。

    【东海期货-2025研报】东海贵金属周度策略:金价高位回落,阶段性回调趋势初现.pdf

    【东海期货-2025研报】东海贵金属周度策略:金价高位回落,阶段性回调趋势初现.pdf

    图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作)

    【资源介绍】 1、该资源包括项目的全部源码,下载可以直接使用! 2、本项目适合作为计算机、数学、电子信息等专业的课程设计、期末大作业和毕设项目,也可以作为小白实战演练和初期项目立项演示的重要参考借鉴资料。 3、本资源作为“学习资料”如果需要实现其他功能,需要能看懂代码,并且热爱钻研和多多调试实践。 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip 图像数据处理工具+数据(帮助用户快速划分数据集并增强图像数据集。通过自动化数据处理流程,简化了深度学习项目的数据准备工作).zip

    diminico_02_0709.pdf

    diminico_02_0709

    agenda_3cd_01_0716.pdf

    agenda_3cd_01_0716

    A课件Python全栈开发线下班.zip

    目录: 第1章 Linux命令入门及VIM编辑器 第2章Python基础 第3章Python面向对象编程 第4章数据结构与算法 第5章UDP与TCP通信 第6章多进程编程 第7章多线程编程 第8章协程 第9章正则表达式 第10章 Http协议 Web服务器并发服务器 第11章网络通信过程 第12章 Python提高1 第13章 Python提高2 第14章 Mysq|基本使用 第15章 Mysq|查询 第16章Mysql与Python交互 第17章Mysql高级 第18章WSGI-miniWeb框架 第19章闭包装饰器 第20章 mini-web框架 添加路由-MySQL功能 第21章 mini-web框架 添加log日志路由支持正则 第22章元类与ORM-面向接口编程

    diminico_02_1108.pdf

    diminico_02_1108

    基于人工智能大模型技术的果蔬农技知识智能问答系统.pdf

    基于人工智能大模型技术的果蔬农技知识智能问答系统.pdf

    diminico_02_0307.pdf

    diminico_02_0307

    dawe_3cd_01_0717.pdf

    dawe_3cd_01_0717

    anslow_3ck_01_0319.pdf

    anslow_3ck_01_0319

    C#全自动多线程上位机源码编程:替代传统PLC触摸屏、以太网通信,强大功能多级页签,支持西门子PLC和OPC,安装KepserverEx5,链接其他数据库,C#多线程自动化工控屏幕上位机源码编程系统:

    C#全自动多线程上位机源码编程:替代传统PLC触摸屏、以太网通信,强大功能多级页签,支持西门子PLC和OPC,安装KepserverEx5,链接其他数据库,C#多线程自动化工控屏幕上位机源码编程系统:功能强大,多级页签,通信灵活,兼容多种配置与数据库连接,C#全自动多线程上位机源码编程 0, 纯源代码。 1, 替代传统plc搭载的触摸屏。 2, 工控屏幕一体机直接和plc通信。 3, 功能强大,多级页签。 4, 可以自由设定串口或以太网通信。 5, 主页。 6, 报警页。 7, 手动调试页。 8, 参数设定页。 9, 历史查询页。 10,系统设定页。 11, 赠送所有控件。 12,使用的西门子Plc。 13,注册opcdaauto.dll组件,用于使用opc。 15,安装kepserverEx5。 16,可以链接其他数据库。 ,核心关键词: C#; 全自动多线程; 上位机源码编程; 纯源代码; PLC替代; 通信; 强大功能; 多级页签; 串口或以太网通信; 主页; 报警页; 手动调试页; 参数设定页; 历史查询页; 系统设定页; 控件赠送; 西门子PLC; OPC

    移动应用开发全流程解析:从创意到上线与推广的最佳实践

    内容概要:本文详细介绍了移动应用开发的全过程,从创意构思和需求分析开始,依次阐述了原型设计、技术选型、前后端开发、测试优化、上线准备到最后的推广和后续维护,帮助读者深入了解和掌握各个环节的要点和最佳实践,特别注重实际操作中的问题和解决方法。文章不仅涵盖技术层面的内容,还包括市场营销和社会影响等方面的探讨。 适合人群:移动应用开发初学者和有一定经验的开发者,想要了解移动应用从构想直到推向市场全部过程的专业人士。 使用场景及目标:指导新创企业和个体开发者从零开始制作自己的应用程序,提供系统的理论知识以及实用技能指导。 阅读建议:本文适合分章节细读,尤其对于每个关键阶段,可以结合具体的案例研究深入理解;在实践应用时应注意参考文中提到的实际开发中容易碰到的问题及其解决方案。

    axios-v0.18.0

    axios-min.js

    Rust语言教程:从入门到进阶 Rust是一门注重性能、内存安全以及并发的系统编程语言 它被设计用来替代C和C++,同时提供更高的安全性和更好的并发支持 本教程将引导你从Rust的基础语法开始,逐步掌

    Rust语言教程:从入门到进阶 Rust是一门注重性能、内存安全以及并发的系统编程语言。它被设计用来替代C和C++,同时提供更高的安全性和更好的并发支持。本教程将引导你从Rust的基础语法开始,逐步掌握到更高级的概念。 一、Rust入门 1. Rust安装 工具链安装:通过rustup安装Rust工具链,它包含Rust编译器、Cargo包管理器以及标准库文档。 验证安装:在终端运行rustc --version和cargo --version来检查Rust和Cargo是否成功安装。 2. Hello, World! 创建一个新的Rust项目:cargo new hello_world --bin。 进入项目目录:cd hello_world。 编辑srcmain.rs文件,添加fn main() { println!(Hello, World!); }。 编译并运行项目:cargo run。 3. Rust基础语法 变量:使用let关键字声明变量,默认情况下变量是不可变的(immutable)。 数据类型:整数(i32, u32等)、浮点数(f32, f64)、布尔值(bool)、字

Global site tag (gtag.js) - Google Analytics