分享
 
 
 

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

王朝other·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

在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的元素确实被删除了。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有