STL Map詳解

關於STL中map的用法剖析【完整版】

1 map概述
    STL(Standard Template Library 標準模版庫)是C++標準程序庫的核心,它深刻影響了標準程序庫的整體結構。STL是一個範型(generic)程序庫,提供一系列軟件方案,利用先進、高效的算法來管理數據。STL的好處在於封裝了許多數據結構和算法(algorithm),map就是其典型代表。
map是STL的一個關聯容器,它提供一對一(key/value 其中第一個可以稱爲關鍵字,每個關鍵字只能在map中出現一次,第二個可以稱爲該關鍵字的值)的數據處理能力,由於這個特性,在處理一對一數據的時候,可以提供編程的快速通道。
2 map的用法
    假設一個班級中,每個學生的學號和他的姓名存在一一映射的關係,這個模型用map可以輕易描述,學號用int描述,姓名用字符串描述,給出map的描述代碼:map<int, string> mapStudent 。
2.1 插入數據
    map元素的插入功能可以通過以下操作實現:
    第一種通過主鍵獲取map中的元素,如果獲取到,則返回對應結點對應的實值(存儲在map結點中的對象)。但這個方法會產生副作用,如果以主鍵“key”獲取結點的實值,在map中並不存在這個結點,則會直接向map中插入以key爲主鍵的結點,並返回這個結點,這時可以對其進行賦值操作。但如果在map中存在了以key爲主鍵的結點,則會返回這個結點的實值,如果此時進行復制操作,則會出現原來結點被新結點覆蓋的危險,如果是指針類型則會出現內存泄漏等問題。由於存在這樣的副作用,不建議使用這種方法進行元素的插入。
    第二種插入value_type數據。
    insert方法接口原型:pair<ierator, bool> insert(const value_type& X)
該方法需要構建一個鍵值對,即value_type,然後調用insert方法,在該方法中實現根據鍵值對中的key值查找對應的結點,如果查找到,則不插入當前結點,並返回找到的那個結點,並將pair中的第二個量置爲false;否則插入當前結點,並返回插入的當前結點,且第二個值置爲true。在插入結點的時候,在map內部會重新構造一個新的value_type結點並將傳入的X進行copy構造,內部使用了placement new方式,通過內存分配器分配一個map結點,再在獲取的結點空間中調用value_type構造函數。所以調用者構造的鍵值對value_type是一個臨時變量,不會加入到map中(不要被引用操作符迷惑,這裏僅僅是傳參效率上的考慮)。這種結點插入的方式是安全的,建議使用這種方式向map中插入元素,並判斷返回的插入結果,根據插入結果進行後續處理。
#pragma warning(disable:4786)
#include <map>
#include <string>
#include <iostream>
using namespace std;
int main()
{
map<int,string> mapStudent;
mapStudent.insert(map<int,string>::value_type(1,"one"));
mapStudent.insert(map<int,string>::value_type(2,"two"));
mapStudent.insert(map<int,string>::value_type(3,"three"));
map<int,string>::iterator iter;
for(iter=mapStudent.begin();iter!=mapStudent.end();iter++)
{
     cout<<iter->first<<" "<<iter->second<<endl;
}
}
2.2 map的大小
    往map中插入了數據,可以用size()函數得到當前已經插入了多少數據:
int nSize=mapStudent.size()
2.3 排序
    STL中默認是採用小於號來排序的,以上代碼在排序上是不存在任何問題的,因爲上面例子中的關鍵字是int型,它本身支持小於號運算。在一些特殊情況下,比如關鍵字是一個結構體,涉及到排序就會出現問題,因爲它沒有小於號運算,insert等函數在編譯的時候過不去。給出一種方法解決排序問題——小於號重載。
#pragma warning(disable:4786)
#include <map>
#include <string>
#include <iostream>
using namespace std;
typedef struct tagStudentInfo
{
int nID;
string strName;
} StudentInfo, *PStudentInfo;   // 學生信息
int main()
{
//用學生信息映射分數
map<StudentInfo, int> mapStudent;
StudentInfo studentInfo;
studentInfo.nID=2;
studentInfo.strName="one";
mapStudent.insert(map<StudentInfo, int>::value_type (studentInfo,90));
studentInfo.nID=1;
studentInfo.strName="two";
mapStudent.insert(map<StudentInfo, int>::value_type (studentInfo,80));
}
以上程序無法編譯通過,需要重載小於號。
typedef struct tagStudentInfo
{
int nID;
string strName;
bool operator <(tagStudentInfo const& _A) const
{//這個函數指定排序策略,按nID排序,如果nID相等按strName排序
if(nID<_A.nID) return true;
if(nID==_A.nID) return strName.compare(_A.strName) <0;
return false;
}
} StudentInfo, *PStudentInfo;
2.4 map中結點的刪除操作
    兩種應用場景:
    第一種:一次只從map中查找一個結點並刪除。
    這種刪除較爲簡單,只需要根據鍵值在map中查找,並將找到的結點刪除就可以了。
#pragma warning(disable:4786)
#include <map>
#include <string>
#include <iostream>
using namespace std;
void main()
{
map<int,string> mapStudent;
mapStudent.insert(map<int,string>::value_type(1,"one"));
mapStudent.insert(map<int,string>::value_type(2,"two"));
mapStudent.insert(map<int,string>::value_type(3,"three"));
map<int,string>::iterator iter;
iter=mapStudent.find(1);
mapStudent.erase(iter);
for(iter=mapStudent.begin();iter!=mapStudent.end();iter++)
{
     cout<<iter->first<<" "<<iter->second<<endl;
}
}
    第二種:從map中遍歷檢查所有結點,將符合條件的結點刪除。
    應用場景描述:系統定期檢查垃圾會話(會話放在map表中),根據當前系統時間減去會話最近活動時間,得到會話最近未活動時間間隔,如果這個間隔超過預定的值(認爲會話是垃圾會話,可以被強制刪除),則刪除該會話並從map中刪除這個結點。
    做法有兩種:
    (1)先遍歷一遍map,找出所有滿足條件的結點,將每一個對應結點的key放入一個vector中,後面再從vector中依次取出key值,做單結點刪除操作。
    這種方法是很原始且效率低下的做法,之所以會這樣實現,是由於開發人員對map使用不甚瞭解的基礎上做出來的。這種方法不但增加了中間處理過程的系統開銷(多構建了一個緩存空間),而且多了N(待刪除結點的結點數)次的查詢操作,對於經常出現的操作,這種低效是不可容忍的。
    (2)在map遍歷的過程中,完成對符合條件結點的刪除操作(這個是由map本身數據結構特性保證的)。在遍歷的過程中最主要的就是怎麼保證刪除的結點在刪除前將指針指向下一個結點(這一點正是我們要做的),在刪除了當前結點後,map中的數據結構能夠保證後續的迭代器指針是有效的,而且後續的結點都沒有遍歷過(這個特性是由map底層的紅黑樹的相關操作保證的)。所以需要將迭代器指向下一個結點後再刪除當前符合條件的結點。
#pragma warning(disable:4786)
#include <map>
#include <string>
#include <iostream>
using namespace std;
void main()
{
map<int,string> mapStudent;
mapStudent.insert(map<int,string>::value_type(1,"one"));
mapStudent.insert(map<int,string>::value_type(2,"two"));
mapStudent.insert(map<int,string>::value_type(3,"three"));
map<int,string>::iterator iter;
string aa="three";
iter=mapStudent.begin();
for(;iter!=mapStudent.end();)
{
     if((iter->second)>=aa)
     {
         //滿足刪除條件,刪除當前結點,並指向下面一個結點
              mapStudent.erase(iter++);
     }
     else
     {
     //條件不滿足,指向下面一個結點
     iter++;
     }
}
for(iter=mapStudent.begin();iter!=mapStudent.end();iter++)
{
     cout<<iter->first<<" "<<iter->second<<endl;
}
}
    這種刪除方式也是STL源碼一書中推薦的方式。比較一下mapStudent.erase(iter++)和mapStudent.erase(iter); iter++;這個執行序列。不妨做個簡單的測試,看看彙編執行下的執行序列:
void func(int a)
{}
int main(int, char**)
{
int iPos=0;
func(iPos++);
}
    函數調用func(iPos++)執行序列:將iPos放入寄存器edx中(緩存起來),隨機對iPos做加操作 inc dword ptr [ebp-0x04]。也就是說函數調用中的iPos++的執行時期在函數體func執行前就已經完成,而函數體中的參數使用的是iPos未做加操作之前的副本。再分析mapStudent.erase(iter++)語句,map中在刪除iter的時候,先將iter做緩存,然後執行iter++使之指向下一個結點,再進入erase函數體中執行刪除操作,刪除時使用的iter就是緩存下來的iter(也就是當前iter(做了加操作之後的iter)所指向結點的上一個結點)。
    根據以上分析,可以看出mapStudent.erase(iter++)和map Student.erase(iter); iter++;這個執行序列是不相同的。前者在erase執行前進行了加操作,在iter被刪除(失效)前進行了加操作,是安全的;後者是在erase執行後才進行加操作,而此時iter已經被刪除(當前的迭代器已經失效了),對一個已經失效的迭代器進行加操作,行爲是不可預期的,這種寫法勢必會導致map操作的失敗並引起進程的異常。
3 結束語
    充分利用map的強大功能,可以使程序員的工作量大大減輕,採用傳統方法編寫的許多行代碼,往往通過調用一兩個算法模板就可實現。map技術可以讓程序員編寫出簡潔而高效的代碼,使編程工作更加簡單而有效。
參考文獻
[1] Nicolai M.Josuttis. C++標準程序庫[M]. 武漢: 華中科技大學出版社,2006.
[2] Scott Meyers. Effective STL中文版——50條有效使用STL的經驗[M]. 北京: 清華大學出版社,2006.
[3] 侯捷. STL源碼剖析[M]. 武漢: 華中科技大學出版社,2002.
[4] 王昌晶,薛錦雲. 從C++到STL[J]. 江西師範大學學報,2004(8):231-234.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章