map应该注意的地方  

在STL(标准模板库)中经常会碰到要删除容器中部分元素的情况,本人在编程中就经常编写这方面的代码,在编码和测试过程中发现在STL中删除容器有很多陷阱,网上也有不少网友提到如何在STL中安全删除元素这些问题。这一次讨论在map(multimap)容器中如何安全的删除和插入元素。

map(multimap)容器为关联式容器,是编程中经常使用的容器,有键值(key)和实值(value),又称字典、映射表。

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

例1:
#pragma warning (disable : 4786)

#include 

#include 

using namespace std;

void main() {

map< int, int* > mapInt;

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

 {
     mapInt[ i ] = new int( i );
}



// 再插入键值为2的元素

mapInt[ 2 ] = new int( 2 );

// 做一些操作

// 删除内存。

map< int, int* >::iterator itMap = mapInt.begin();

for ( ; itMap != mapInt.end(); ++itMap ) {

delete itMap->second;

}

}



例2:

void main() {

map< int, int* > mapInt;

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

mapInt.insert( make_pair( i, new int( i ) ) );

}


// 再插入键值为2的元素

mapInt.insert( make_pair( 2, new int( 2 ) ) );

// 做一些操作

// 删除内存。

map< int, int* >::iterator itMap = mapInt.begin();

for ( ; itMap != mapInt.end(); ++itMap ) {

delete itMap->second;

}
}


例3:
void main() {

map< int, int > mapInt;

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

mapInt.insert( make_pair( i, i ) );

}



mapInt.insert( make_pair( 5, 2 ) );

// 删除所有实值为2的元素

map< int, int >::iterator itMap = mapInt.begin();

for ( ; itMap != mapInt.end(); ++itMap ) {//写书店类的售出函数时,我就在这犯错了。

if ( itMap->second == 2 ) {

mapInt.erase( itMap );

}


}

}


分析:

例1将导致内存泄露,因为mapInt[ 2 ] = new int( 2 );这条语句把原来键值为2的元素的实值指针覆盖了,原来的指针就成为野指针,导致内存泄露。

例2也将导致内存泄露,因为mapInt.insert( make_pair( 2, new int( 2 ) ) );这条语句因为键值为2的元素已经存在,导致插入元素失败,所以指向刚分配的内存的指针成为野指针,导致内存泄露。
map容器插入元素的方法。可以调用map容器的insert成员函数插入元素,或者直接用map容器的下标运算式赋值,但这里有一个地方要注意,如果实值为指针的话,插入重复键值的元素时,会导致内存泄露。所以对于插入元素时,必须检查是否插入成功。

正确的方法:

void main() {
map< int, int* > mapInt;
bool bRet;
for ( int i = 0; i < 5; i++ ) {
int* pI = new int( i );
bRet = mapInt.insert( make_pair( i, pI ) ).second;
if ( !bRet ) {
// 插入元素不成功。
delete pI;
}
}

// 再插入键值为2的元素

int* pI = new int( 2 );
bRet = mapInt.insert( make_pair( 2, pI ) ).second;
if ( !bRet ) {

// 插入元素不成功。

delete pI;
}

// 做一些操作

// 删除内存。

map< int, int* >::iterator itMap = mapInt.begin();

for ( ; itMap != mapInt.end(); ++itMap ) {

delete itMap->second;

}
}


例3将导致程序未定义的错误,在windows中即是访问非法内存,程序当掉。因为mapInt.erase( itMap );调用后itMap迭代器已无效了,所以当执行++itMap时,访问非法内存,导致程序当掉。

如果erase()总是返回下一元素的位置,那就可以像在vector容器中删除元素一样,(但在 VC中好像并不是返回下一个元素位置)如:

// 删除所有实值为2的元素

map< int, int >::iterator itMap = mapInt.begin();

for ( ; itMap != mapInt.end(); ) {

if ( itMap->second == 2 ) {

itMap = mapInt.erase( itMap );

}

else {

++itMap;
}
}

但是,注意,以上的方式只在vc使用P.J.STL中才能编译通过,而使用SGI STL库则编译不过,因为SGISTL库在设计中考虑到如果用户不需要这一特性,就会损失性能,因此否决了这种想法。所以要保证可移植性, 最好采用下面的方法:

// 删除所有实值为2的元素
map< int, int >::iterator itMap = mapInt.begin();

for ( ; itMap != mapInt.end(); ) {

if ( itMap->second == 2 ) {

// itMap++将使itMap指向下一个元素,但返回原迭代器的副本,因为++在后不是在前.所以

// erase()被调用时,itMap不指向将要被删除的元素

mapInt.erase( itMap++ ); 

}

else

 {
    ++itMap;
 }

}
 
关于常量迭代器:

你是否疑惑,为什么C++标准化委员会使用常量迭代器(const_iterator)代替常量迭代型(const iterator)?为什么不用const_iterator代替 const iterator ?

原因很简单:常量迭代器不是常量迭代型。这意味着你能够改变常量迭代器的长度(例如:用++来增加长度)。然而,你不能改变常量迭代器所指向的对象的值。

让我们进一步阐明一下。如果你把迭代器考虑成指向T的指针,那么常量迭代器不是指向T的常量指针,而是指向常量T的非常量指针。

 在接下来的例子中,常量迭代器的长度成功地增加了。而另一方面,想要通过迭代器,而改变其中的元素值的企图却失败了,因为它是常量迭代器。

std::vector<double> results;

results:push_back(98.99);

results:push_back(86.33);

std::vector<double>::const_iterator it = vi.begin();

++it; file://正确,改变迭代器,而不是其中的元素

*it = 100.5; file://错误,不能用const_iterator改变其中元素

总之,const_iterator的使用者,不允许通过其迭代器,而改变其指向的元素值。

 

常量迭代器不能执行erase操作

#include <map>
#include <iostream>

int main( )
{
   using namespace std;
   map <int, int> m1;

   map <int, int> :: iterator m1_Iter;
   map <int, int> :: const_iterator m1_cIter;
   typedef pair <int, int> Int_Pair;

   m1.insert ( Int_Pair ( 1, 10 ) );
   m1.insert ( Int_Pair ( 2, 20 ) );
   m1.insert ( Int_Pair ( 3, 30 ) );

   m1_cIter = m1.end( );
   m1_cIter--;
   cout << "The value of the last element of m1 is:\n" 
        << m1_cIter -> second << endl;
   
   m1_Iter = m1.end( );
   m1_Iter--;
   m1.erase ( m1_Iter );

   // The following 2 lines would err because the iterator is const
   // m1_cIter = m1.end ( );
   // m1_cIter--;
   // m1.erase ( m1_cIter );

   m1_cIter = m1.end( );
   m1_cIter--;
   cout << "The value of the last element of m1 is now:\n"
        << m1_cIter -> second << endl;
}


 

 1、关于vector中元素的删除和迭代器失效问题

01.vector<int> vv;  
02.vv.push_back(1);                                //加入第一个元素   
03.vector<int>::iterator itBegin = vv.begin();   //获取第一个元素迭代器*itBegin=1   
04.vv.push_back(2);                                //因为预留不够,所以发生内存搬移,之前迭代器将全部失效   
05.vv.push_back(1);                                //即是说itBegin现在已经是个无效迭代器   
06.vv.push_back(1);                                //所以使用vector迭代器一定要小心失效!   
07.vv.push_back(3);                                //移动、增加、插入、删除以及reserve、resize都可能使迭代器无效!   
08.vv.push_back(4);  
09.vv.push_back(3);  
10.vv.push_back(5);  
11.vv.push_back(6);  
12.int n = vv.size();      //n = 9   
13.  
14.vector<int>::iterator itrmv = remove(vv.begin(), vv.end(), 3);    //结果:1,2,1,1,4,5,6,5,6   
15.n = vv.size();          //n = 9   
16.  
17.//删除vector中等于某值的所有元素   
18.//remove算法只是对容器中有效元素向前移动覆盖无效元素,返回第一个无效元素指针   
19.//1、它不会有删除动作 2、尾部无效元素没有意义 3、之后容器size不变   
20.  
21.vv.erase(itrmv, vv.end());          //结果:1,2,1,1,4,5,6   
22.n = vv.size();                      //n = 7   
23.  
24.bool BeDelete(int n)  
25.{  
26.    return n == 1 || n == 2;  
27.}  
28.  
29.//借助remove_if算法删除vecotr中符合某些条件的所有元素   
30.  
31.vv.erase(remove_if(vv.begin(), vv.end(), BeDelete), vv.end());  //结果:4,5,6   
32.n = vv.size();      //n = 3   
33.  
34.//若用循环实现删除,需要注意erase后迭代器失效问题   
35.for(vector<int>::iterator it=vv.begin(); it!=vv.end(); )  
36.{  
37.    if(*it == 4)  
38.    {  
39.        /*错误的做法 
40.            vv.erase(it);   //对vector进行增加删除等操作后之前it可能无效 
41.            it++;           //it此时已经无效 
42.        */  
43.        /*错误的做法 
44.            vv.erase(it++); //erase后元素发生了移动所以it多向后跳过一个元素 
45.        */  
46.        it = vv.erase(it);  //正确的做法,erase返回下一个有效it,此方法适用于List,vector,deque容器  
47.    }  
48.    else  
49.    {  
50.        it++;  
51.    }  
52.}  
53.  
54.n = vv.size();              //n=2,结果:5,6  


 

2、释放vector容器多余的内存

01.vector<int> vn;  
02.vn.reserve(10);             //预留10个元素空间   
03.int nn = vn.capacity();     //nn = 10   
04.  
05.vn.push_back(1);  
06.vn.push_back(2);  
07.nn = vn.capacity();         //nn = 10   
08.  
09.vector<int>(vn).swap(vn); //通过建立一个新对象释放多余空间   
10.nn = vn.capacity();         //nn = 2   
11.nn = vn.size();             //nn = 2   
12.  
13.vector<int>().swap(vn);   //会完全清空容器,释放所有空间 


 

3、map中删除满足某些条件的元素

01.map<int, int> mm;  
02.mm.insert(make_pair(1,1));  
03.mm.insert(make_pair(2,2));  
04.mm.insert(make_pair(3,1));  
05.mm.insert(make_pair(4,3));  
06.mm.insert(make_pair(5,3));  
07.mm.insert(make_pair(6,6));  
08.  
09.//注意:对map和set等自动排序的容器不应使用remove一类算法   
10.//应使用for+erase或者while+find_if+erase   
11.  
12.  
13.//第一种方法for+erase   
14.map<int, int>::iterator mit;  
15.  
16.for(mit = mm.begin(); mit != mm.end();)  
17.{  
18.    if(mit->second == 1)  
19.    {  
20.        mm.erase(mit++);    //这里需要注意,此方法适用于List,map,set容器 
21.    }  
22.    else  
23.    {  
24.        mit++;  
25.    }  
26.}                     
27.  
28.//第二种方法,while+find_if+erase   
29.  
30.//仅以元素做条件检索   
31.bool mBeDelete(const pair<int, int>& val)  
32.{  
33.    return val.second == 1;  
34.}  
35.  
36.mit = find_if(mm.begin(), mm.end(), mBeDelete);  
37.while(mit != mm.end())  
38.{  
39.    mit = find_if(mm.erase(mit), mm.end(), mBeDelete);  
40.}  
41.  
42.//除元素外还需要传入另外一个条件参数   
43.  
44.//这里的参数无法使用常量引用   
45.bool mBeDelete2(pair<int, int> val, int n)  
46.{  
47.    return val.second == n;  
48.}  
49.  
50.mit = find_if(mm.begin(), mm.end(), bind2nd(ptr_fun(mBeDelete2),3));  
51.while(mit != mm.end())  
52.{  
53.    mit = find_if(mm.erase(mit), mm.end(), bind2nd(ptr_fun(mBeDelete2),3));  
54.}  

C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在STL使用过程中,并不会感到陌生。

关于set,必须说明的是set关联式容器。set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。

(1)为何map和set的插入删除效率比用其他序列容器高?

大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:

 

  A
   / \
  B C
 / \ / \
  D E F G

因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。

(2)为何每次insert之后,以前保存的iterator不会失效?

iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。但相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。

(3)当数据元素增多时,set的插入和搜索速度变化如何?

如果你知道log2的关系你应该就彻底了解这个答案。在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了


 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐