• 转载: STL序列式容器中删除元素的方法和陷阱一 .


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

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

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

    例 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;

           }

     

    }

    例 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;

           }

    }

     

    分析:

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

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

    例 2 可能会导致不能把 vectInt 中所有为 3 的元素删除掉。因为第一次删除成功时, itVect = vectInt.erase( itVect );itVect 为指向 3 之后的位置,之后再执行 ++itVect , itVect 就掉过了被删除元素 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 的初始状态为:

                                                                                 | end

    0        1        2        3         4     

                                         

    删除 3 后,

                                                                     | 新的 end  | 原来的 end

    0         1        2        4         4     

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

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

    如果把 “ vectInt [ 4 ] = 4; ” 这一行改为 “ vectInt [ 4 ] = 3; ”过程如下:

                                                                            | end

    0        1        2        3         3     

    删除 3 后,

                                                                     | 新的 end       | 原来的 end

    0        1        2        3         3      

    删除第 2 个 3 后,

                                                     | 新的 end            | 原来的 end

    0        1        2        3         3     

    这时 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 循环中计算,不要因为微小的优化而导致程序出错。

    正确的方法:

    例 4 :

    #include <iostream>

    #include <vector>

    using namespace std;

    void main( ) {

           vector<int> vectInt;

           int i;

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

                  vectInt.push_back( i );

                  if ( 3 == i ) {

                         //       使 3 的元素有两个,并且相临。

                         vectInt.push_back( i );

                  }

           }

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

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

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

                  if ( *itVect == 3 ) {

                         itVect = vectInt.erase( itVect );

                  }

                  else {

                         ++itVect;

                  }

           }

           //     把 vectInt.size() 放在 for 循环中

          for (  i = 0 ; i < vectInt.size(); i++ )  {

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

           }

    运行结果为:

    i= 0, 0

    i= 1, 1

    i= 2, 2

    i= 3, 4

    从结果显示值为 3 的元素确实被删除了。

  • 相关阅读:
    记录一下 一个复杂SQL的执行效率分析
    1582 Incorrect parameter count in the call to native function 'FIND_IN_SET'
    教我兄弟学Android逆向10 静态分析反调试apk
    教我兄弟学Android逆向09 IDA动态破解登陆验证
    教我兄弟学Android逆向08 IDA爆破签名验证
    教我兄弟学Android逆向07 IDA破解第一个so
    教我兄弟学Android逆向06 用AndroidStudio编写第一个so
    教我兄弟学Android逆向05 在smali代码中插入Log
    教我兄弟学Android逆向04 动态调试smali代码
    教我兄弟学Android逆向03 破解第一个Android游戏
  • 原文地址:https://www.cnblogs.com/kex1n/p/2283613.html
Copyright © 2020-2023  润新知