《C++Primer》第十章-關聯容器-學習筆記(1)

《C++Primer》第十章-關聯容器-學習筆記(1)

日誌:
1,2020-03-14 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
《C++Primer》第十二章-類-學習筆記(1)

摘要

本章涵蓋了關聯容器(標準庫容器類型)的相關內容,並完善和擴展了一個使用順序容器和關聯容器的例子。
關聯容器和順序容器的本質差別在於:關聯容器通過鍵(key)存儲和讀取元素,而順序容器則通過元素在容器中的位置順序存儲和訪問元素。雖然關聯容器的大部分行爲與順序容器相同,但其獨特之處在於支持鍵的使用。
關聯容器(Associative containers)支持通過鍵(key)來高效地查找和讀取元素。
兩個基本的關聯容器類型是 mapset

  • map的元素以鍵-值(key-value)對的形式組織:鍵(key)用作元素在 map 中的索引,而值(value)則表示所存儲和讀取的數據。
  • set僅包含一個鍵(key),並有效地支持關於某個鍵是否存在的查詢。

一般來說,如果希望有效地存儲不同值的集合,那麼使用 set 容器比較合適,而 map 容器則更適用於需要存儲(乃至修改)每個鍵所關聯的值的情況。
在做某種文本處理時,可使用 set 保存要忽略的單詞。而字典則是 map 的一種很好的應用:單詞本身是鍵,而它的解釋說明則是值。
set 和 map 類型的對象所包含的元素都具有不同的鍵,不允許爲同一個鍵添加第二個元素。如果一個鍵必須對應多個實例,則需使用 multimapmultiset,這兩種類型允許多個元素擁有相同的鍵。
關聯容器支持很多順序容器也提供的相同操作,此外,還提供管理或使用鍵的特殊操作。下面將詳細討論關聯容器類型及其操作,最後以一個用容器實現的小型文本查詢程序結束本章。

表1 關聯容器類型:

關聯容器類型 作用
map 關聯數組:元素通過鍵(key)來存儲和讀取
set 大小可變的集合,支持通過鍵實現的快速讀取
multimap 支持同一個鍵多次出現的 map 類型
multiset 支持同一個鍵多次出現的 set 類型

引言:pair 類型

在開始介紹關聯容器之前,必須先了解一種與之相關的簡單的標準庫類型——pair(表 2),該類型在 utility 頭文件中定義。
表 2 pair類型提供的操作:

pair類型操作 作用
pair<T1, T2> p1; 創建一個空的 pair 對象,它的兩個元素分別是 T1 和 T2類型,採用值初始化(第 3.3.1 節)
pair<T1, T2> p1(v1, v2); 創建一個 pair 對象,它的兩個元素分別是 T1 和 T2 類型,其中 first 成員初始化爲 v1,而 second 成員初始化爲 v2
make_pair(v1,v2) 以 v1 和 v2 值創建一個新 pair 對象,其元素類型分別是v1 和 v2 的類型
p1 < p2 兩個 pair 對象之間的小於運算,其定義遵循字典次序:如果 p1.first < p2.first 或者!(p2.first < p1.first) &&p1.second < p2.second,則返回 true
p1 == p2 如果兩個 pair 對象的 first 和 second 成員依次相等,則這兩個對象相等。該運算使用其元素的 == 操作符
p.first 返回 p 中名爲 first 的(公有)數據成員
p.second 返回 p 的名爲 second 的(公有)數據成員

pair 的創建和初始化

pair 只包含兩個數據值與容器一樣,pair 也是一種模板類型。但又與之前介紹的容器不同,在創建 pair 對象時,必須提供兩個類型名:pair 對象所包含的兩個數據成員各自對應的類型名字,這兩個類型名字不必相同。

pair<string, string> anon; // holds two strings
pair<string, int> word_count; // holds a string and an int
pair<string, vector<int> > line; // holds string and vector<int>

如果在創建 pair 對象時不提供初始化式,則調用默認構造函數對其成員採用值初始化。於是,anon 是包含兩空 string 類型成員的 pair 對象,line 則存儲一個空的 string 類型對象和一個空的vector 類型對象。word_count 中的 int 成員獲得 0 值,而 string 成員則初始化爲空 string 對象。
當然,也可在定義時爲每個成員提供初始化式:

pair<string, string> author("James", "Joyce");

創建一個名爲 author 的 pair 對象,它的兩個成員都是 string 類型,分別初始化爲字符串 “James” 和 “Joyce”。

pair 類型的使用相當繁瑣,因此,如果需要定義多個相同的 pair 類型對象,可考慮利用 typedef 簡化其聲明:

typedef pair<string, string> Author;
Author proust("Marcel", "Proust");
Author joyce("James", "Joyce");

pairs 對象的操作

與其他標準庫類型不同,對於 pair 類,可以直接訪問其數據成員其成員都是僅有的分別命名爲 first 和 second。只需使用普通的點操作符——成員訪問標誌即可訪問其成員:

string firstBook;
// access and test the data members of the pair
if (author.first == "James" && author.second == "Joyce")
firstBook = "Stephen Hero";

標準庫只爲 pair 類型定義了表 2 所列出的數量有限的操作。

生成新的 pair 對象

除了構造函數,標準庫還定義了一個make_pair 函數,由傳遞給它的兩個實參生成一個新的 pair 對象。可如下使用make_pair函數創建新的 pair 對象,並賦給已存在的 pair 對象:

pair<string, string> next_auth;
string first, last;
while (cin >> first >> last) {
// generate a pair from first and last
next_auth = make_pair(first, last);
// process next_auth...
}

這個循環處理一系列的作者信息:在 while 循環條件中讀入的作者名字作爲實參,調用 make_pair 函數生成一個新的 pair 對象。此操作等價於下面更復雜的操作:

// use pair constructor to make first and last into a pair
next_auth = pair<string, string>(first, last);

由於 pair 的數據成員是公有的,因而可如下直接地讀取輸入:

pair<string, string> next_auth;
// read directly into the members of next_auth
while (cin >> next_auth.first >> next_auth.second) {
// process next_auth...
}

關聯容器

關聯容器共享大部分但並非全部的順序容器操作。關聯容器不提供front、 push_front、 pop_front、back、push_back 以及 pop_back 操作。
順序容器和關聯容器公共的操作包括下面的幾種:
• 表 2 描述的前三種構造函數:

C<T> c; // creates an empty container   創建空的容器
// c2 must be same type as c1
C<T> c1(c2); // copies elements from c2 into c1  
// b and e are iterators denoting a sequence
C<T> c(b, e); // copies elements from the sequence into c

• 第 9.3.4 節中描述的關係運算。
• 表 9.6 列出的 begin、end、rbegin 和 rend 操作。
• 表 9.5 列出的類型別名(typedef)。注意,對於 map 容器,value_type並非元素的類型,而是描述鍵及其關聯值類型的 pair 類型。第 10.3.2節 將詳細解釋 map 中的類型別名。
• 表 9.11 中描述的 swap 和賦值操作。但關聯容器不提供 assign 函數。
• 表 9.10 列出的 clear 和 erase 操作,但關聯容器的 erase 運算返回void 類型。
• 表 9.8 列出的關於容器大小的操作。但 resize 函數不能用於關聯容器。

根據鍵排列元素

除了上述列出的操作之外,關聯容器還提供了其他的操作。而對於順序容器也提供的相同操作,關聯容器也重新定義了這些操作的含義或返回類型,其中的差別在於關聯容器中使用了鍵
容器元素根據鍵的次序排列”這一事實就是一個重要的結論:在迭代遍歷關聯容器時,我們可確保按鍵的順序的訪問元素,而與元素在容器中的存放位置完全無關。

map 類型

map是鍵-值對的集合。map 類型通常可理解爲關聯數組(associative array):可使用鍵作爲下標來獲取一個值,正如內置數組類型一樣。而關聯的本質在於元素的值與某個特定的鍵相關聯,而並非通過元素在數組中的位置來獲取。

map 對象的定義

要使用 map 對象,則必須包含 map 頭文件。在定義 map 對象時,必須分別指明鍵和值的類型(value type)(表 10.4):

// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int

這個語句定義了一個名爲 word_count 的 map 對象,由 string 類型的鍵索引,關聯的值則 int 型。

表 3. map 的構造函數

map 的構造函數 作用
map<k, v> m; 創建一個名爲 m 的空 map 對象,其鍵和值的類型分別爲 k 和 v
map<k, v> m(m2); 創建 m2 的副本 m,m 與 m2 必須有相同的鍵類型和值類型
map<k, v> m(b, e); 創建 map 類型的對象 m,存儲迭代器 b 和 e 標記的範圍內所有元素的副本。元素的類型必須能轉換爲 pair<const k, v>

鍵類型的約束

使用關聯容器時,它的鍵不但有一個類型,而且還有一個相關的比較函數。默認情況下,標準庫使用鍵類型(key type)定義的<操作符來實現鍵的比較。第 15.8.3 節將介紹如何重寫默認的操作符,並提供自定義的操作符函數。
所用的比較函數必須在鍵類型上定義嚴格弱排序(strict weak ordering)。所謂的嚴格弱排序可理解爲鍵類型數據上的“小於”關係,雖然實際上可以選擇將比較函數設計得更復雜。但無論這樣的比較函數如何定義,當用於一個鍵與自身的比較時,肯定會導致 false 結果。此外,在比較兩個鍵時,不能出現相互“小於”的情況(即A<B和B<A是不能同時發生),而且,如果 k1“小於”k2,k2“小於”k3,則 k1 必然“小於”k3。對於兩個鍵,如果它們相互之間都不存在“小於”關係,則容器將之視爲相同的鍵。用做 map 對象的鍵時,可使用任意一個鍵值來訪問相應的元素。

在實際應用中,鍵類型必須定義 < 操作符,而且該操作符應能“正確地工作”這一點很重要
例如,在書店問題中,可增加一個名爲 ISBN 的類型,封裝與國際標準圖書編號(ISBN)相關的規則。在我們的實現中,國際標準圖書編號是 string 類型,可做比較運算以確定編號之間的大小關係。因此,ISBN 類型可以支持 < 運算。假設我們已經定義了這樣的類型,則可定義一個 map 容器對象,以便高效地查找書店中存放的某本書。

map<ISBN, Sales_item> bookstore;

該語句定義了一個名爲 bookstore 的 map 對象,以 ISBN 類型的對象爲索引,其所有元素都存儲了一個關聯的 Sales_item 類類型實例。對於鍵類型,唯一的約束就是必須支持 < 操作符,至於是否支持其他的關係或相等運算,則不作要求

map 定義的類型

map 對象的元素是鍵-值對,也即每個元素包含兩個部分:鍵以及由鍵關聯的值map 的 value_type 就反映了這個事實。該類型比前面介紹的容器所使用的元素類型要複雜得多:value_type 是存儲元素的鍵以及值的 pair 類型,而且鍵爲 const。例如,word_count 數組的 value_type 爲 pair<const string,
int> 類型。
表 10.4. map 類定義的類型

map 類定義的類型 作用
map<K,V>::key_type 在 map 容器中,用做索引的鍵的類型
map<K,V>::mapped_type 在 map 容器中,鍵所關聯的值的類型
map<K,V>::value_type 一個 pair 類型,它的first 元素具有 const map<K,V>::key_type 類型,而 second 元素則爲 map<K,V>::mapped_type 類型

在學習 map 的接口時,需謹記 value_type 是 pair 類型,它的值成員可以修改,但鍵成員不能修改。

map 迭代器進行解引用將產生 pair 類型的對象

對容器迭代器進行解引用時,將獲得一個引用,指向容器中一個 value_type 類型的值。對於 map 容器,其 value_type 是 pair 類型:

// get an iterator to an element in word_count
map<string, int>::iterator map_it = word_count.begin();
// *map_it is a reference to a pair<const string, int> object
cout << map_it->first; // prints the key for this element
cout << " " << map_it->second; // prints the value of the element
map_it->first = "new key"; // error: key is const
++map_it->second; // ok: we can change value through an iterator

對迭代器進行解引用將獲得一個 pair 對象,它的 first 成員存放鍵,爲const,而 second 成員則存放值。

map 容器額外定義的類型別名(typedef)

map 類額外定義了兩種類型:key_type 和 mapped_type,以獲得鍵或值的類型。對於 word_count,其 key_type 是 string 類型,而 mapped_type 則是int 型。如同順序容器(第 9.3.1 節)一樣,可使用作用域操作符(scope operator)來獲取類型成員,如 map<string, int>::key_type。

map 添加元素

定義了 map 容器後,下一步工作就是在容器中添加鍵-值元素對。該項工作可使用 insert 成員實現;或者,先用下標操作符獲取元素,然後給獲取的元素賦值。在這兩種情況下,一個給定的鍵只能對應於一個元素這一事實影響了這些操作的行爲。

使用下標訪問 map 對象

如下編寫程序時:

map <string, int> word_count; // empty map
// insert default initialzed element with key Anna; then assign 1 to its value
word_count["Anna"] = 1;

將發生以下事情:

  1. 在 word_count 中查找鍵爲 Anna 的元素,沒有找到。
  2. 將一個新的鍵-值對插入到 word_count 中。它的鍵是 const string 類型的對象,保存 Anna。而它的值則採用值初始化,這就意味着在本例中值爲 0。
  3. 將這個新的鍵-值對插入到 word_count 中。
  4. 讀取新插入的元素,並將它的值賦爲 1。

使用下標訪問 map 與使用下標訪問數組或 vector 的行爲截然不同:用下標訪問不存在的元素將導致在 map 容器中添加一個新元素,它的鍵即爲該下標值。

如同其他下標操作符一樣,map 的下標也使用索引(其實就是鍵)來獲取該鍵所關聯的值

  • 如果該鍵已在容器中,則 map 的下標運算與 vector 的下標運算行爲相同:返回該鍵所關聯的值。
  • 只有在所查找的鍵不存在時,map 容器才爲該鍵創建一個新的元素,並將它插入到此 map 對象中。此時,所關聯的值採用值初始化:類類型的元素用默認構造函數初始化,而內置類型的元素初始化爲 0。

下標操作符返回值的使用

通常來說,下標操作符返回左值。它返回的左值是特定鍵所關聯的值。可如下讀或寫元素:

cout << word_count["Anna"]; // fetch element indexed by Anna; prints 1
++word_count["Anna"]; // fetch the element and add one to it
cout << word_count["Anna"]; // fetch the element and print it; prints 2

有別於 vector 或 string 類型,map 下標操作符返回的類型與對 map 迭代器進行解引用獲得的類型不相同。
顯然,map 迭代器返回 value_type 類型的值——包含 const key_type 和mapped_type 類型成員的 pair 對象;下標操作符則返回一個 mapped_type 類型的值。

下標行爲的編程意義

對於 map 容器,如果下標所表示的鍵在容器中不存在,則添加新元素,這一特性可使程序驚人地簡練

// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int string word;
while (cin >> word)
	++word_count[word];
//文件計數程序map(如果下標所表示的鍵在容器中不存在,可行)

這段程序創建一個 map 對象,用來記錄每個單詞出現的次數。while 循環每次從標準輸入讀取一個單詞。如果這是一個新的單詞,則在 word_count 中添加以該單詞爲索引的新元素。如果讀入的單詞已在 map 對象中,則將它所對應的值加 1。
其中最有趣的是,在單詞第一次出現時,會在 word_count 中創建並插入一個以該單詞爲索引的新元素,同時將它的值初始化爲 0。然後其值立即加 1,所以每次在 map 中添加新元素時,所統計的出現次數正好從 1 開始。

map::insert 的使用

map 容器的 insert 成員與順序容器的類似,但有一點要注意:必須考慮鍵的作用。鍵影響了實參的類型:插入單個元素的 insert 版本使用鍵-值 pair類型的參數。類似地,對於參數爲一對迭代器的版本迭代器必須指向鍵-值pair 類型的元素。另一個差別則是:map 容器的接受單個值的 insert 版本的返回類型。本節的後續部分將詳細闡述這一特性。
表 10.5. map 容器提供的 insert 操作

map 的 insert 操作 作用
m.insert(e) e 是一個用在 m 上的 value_type 類型的值。如果鍵(e.first)不在 m 中,則插入一個值爲 e.second 的新元素;如果該鍵在 m 中已存在,則保持 m 不變。該函數返回一個pair 類型對象包含指向鍵爲 e.first 的元素的 map 迭代器,以及一個 bool 類型的對象,表示是否插入了該元素
m.insert(beg,end) beg 和 end 是標記元素範圍的迭代器,其中的元素必須爲m.value_type 類型的鍵-值對。對於該範圍內的所有元素,如果它的鍵在 m 中不存在,則將該鍵及其關聯的值插入到 m。(返回 void 類型)
m.insert(iter,e) e 是一個用在 m 上的 value_type 類型的值。如果鍵(e.first)不在 m 中,則創建新元素,並以迭代器 iter 爲起點搜索新元素存儲的位置。(返回一個迭代器,指向 m 中具有給定鍵的元素)。

以 insert 代替下標運算

使用下標給 map 容器添加新元素時,元素的值部分將採用值初始化。通常,我們會立即爲其賦值,其實就是對同一個對象進行初始化並賦值。而插入元素的另一個方法是:直接使用 insert 成員給 map 容器添加新元素,其語法更緊湊:

// if Anna not already in word_count, inserts new element with value 1
word_count.insert(map<string, int>::value_type("Anna", 1));

這個 insert 函數版本的實參:

map<string, int>::value_type(anna, 1)

是一個新創建的 pair 對象,將直接插入到 map 容器中。謹記 value_type是 pair<const K, V> 類型的同義詞,K 爲鍵類型,而 V 是鍵所關聯的值的類型。insert 的實參創建了一個適當的 pair 類型新對象,該對象將插入到 map容器。在添加新 map 元素時,使用 insert 成員可避免使用下標操作符所帶來的副作用:不必要的初始化。
傳遞給 insert 的實參相當笨拙。可用兩種方法簡化:使用 make_pair:

word_count.insert(make_pair("Anna", 1));

或使用 typedef

typedef map<string,int>::value_type valType;
word_count.insert(valType("Anna", 1));

這兩種方法都使用調用變得簡單,提高了程序的可讀性。

檢測 insert 的返回值

map 對象中一個給定鍵只對應一個元素。如果試圖插入的元素所對應的鍵已在容器中,則 insert 將不做任何操作。含有一個或一對迭代器形參的 insert函數版本並不說明是否有或有多少個元素插入到容器中。但是,帶有一個鍵-值 pair 形參的 insert 版本將返回一個值:包含一個迭代器和一個 bool 值的 pair 對象,其中迭代器指向 map 中具有相應鍵的元素,而 bool 值則表示是否插入了該元素。
如果該鍵已在容器中,則其關聯的值保持不變,返回的 bool 值爲 true。在這兩種情況下,迭代器都將指向具有給定鍵的元素。

下面是使用 insert 重寫的單詞統計程序:

// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int string word;
while (cin >> word) {
// inserts element with key equal to word and value 1;
// if word already in word_count, insert does nothing
pair<map<string, int>::iterator, bool> ret =word_count.insert(make_pair(word, 1));
if (!ret.second) // word already in word_count
	++ret.first->second; // increment counter
}
//單詞統計程序(鍵可能在容器中存在的)

對於每個單詞,都嘗試 insert 它,並將它的值賦 1。if 語句檢測 insert函數返回值中的 bool 值。如果該值爲 false,則表示沒有做插入操作,按 word索引的元素已在 word_count 中存在。此時,將該元素所關聯的值加 1。

語法展開

上面代碼中ret 的定義和自增運算可能比較難解釋:

pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1));

首先,應該很容易看出我們定義的是一個 pair 對象,它的 second 成員爲bool 類型。而它的 first 成員則比較難理解,這是 map<string, int> 容器所定義的迭代器類型。
根據操作符的優先級次序(第 5.10.1 節),可如下從添加圓括號開始理解自增操作:

++((ret.first)->second); // equivalent expression

下面對這個表達式一步步地展開解釋:
• ret 存儲 insert 函數返回的 pair 對象。該 pair 的 first 成員是一個 map 迭代器,指向插入的鍵。
• ret.first 從 insert 返回的 pair 對象中獲取 map 迭代器。
• ret.first->second 對該迭代器進行解引用,獲得一個 value_type 類型的對象。這個對象同樣是 pair 類型的,它的 second 成員即爲我們所添加的元素的值部分。
• ++ret.first->second 實現該值的自增運算。
歸結起來,這個自增語句獲取指向按 word 索引的元素的迭代器,並將該元素的值加 1。

查找並讀取 map 中的元素

下標操作符給出了讀取一個值的最簡單方法:

map<string,int> word_count;
int occurs = word_count["foobar"];

但是,使用下標存在一個很危險的副作用:如果該鍵不在 map 容器中,那麼下標操作會插入一個具有該鍵的新元素。這樣的行爲是否正確取決於程序員的意願
在這個例子中,如果“foobar”不存在,則在 map 中插入具有該鍵的新元素,其關聯的值爲 0。在這種情況下,occurs 獲得 0 值。
我們的單詞統計程序的確是要通過下標引用一個不存在的元素來實現新元素的插入,並將其關聯的值初始化爲 0。然而,大多數情況下,我們只想知道某元素是否存在,而當該元素不存在時,並不想做做插入運算。對於這種應用,則不能使用下標操作符來判斷元素是否存在。
map 容器提供了兩個操作:count 和 find,用於檢查某個鍵是否存在而不會插入該鍵。
表 10.6. 不修改 map 對象的查詢操作:

map查詢操作 操作 作用
m.count(k) 返回 m 中 k 的出現次數
m.find(k) 如果 m 容器中存在按 k 索引的元素,則返回指向該元素的迭代器。如果不存在,則返回超出末端迭代器(第 3.4 節)

使用 count 檢查 map 對象中某鍵是否存在

對於 map 對象,count 成員的返回值只能是 0 或 1。因爲map 容器只允許一個鍵對應一個實例,所以 count 可有效地表明一個鍵是否存在
而對於multimaps容器,count 的返回值將有更多的用途,相關內容將會在之後介紹。如果返回值非 0,則可以使用下標操作符來獲取該鍵所關聯的值,而不必擔心這樣做會在 map 中插入新元素:

int occurs = 0;
if (word_count.count("foobar"))
	occurs = word_count["foobar"];

當然,在執行 count 後再使用下標操作符,實際上是對元素作了兩次查找。如果希望當元素存在時就使用它,則應該用 find 操作。

讀取元素而不插入該元素

find 操作返回指向元素的迭代器,如果元素不存在,則返回 end 迭代器:

int occurs = 0;
map<string,int>::iterator it = word_count.find("foobar");
if (it != word_count.end())
	occurs = it->second;

如果希望當具有指定鍵的元素存在時,就獲取該元素的引用,否則就不在容器中創建新元素,那麼應該使用 find。

從 map 對象中刪除元素

map 容器中刪除元素erase 操作三種變化形式(表 10.7)。與順序容器一樣,可向 erase 傳遞一個或一對迭代器,來刪除單個元素或一段範圍內的元素。其刪除功能類似於順序容器,但有一點不同:map 容器的 erase 操作返回 void,而順序容器的 erase 操作則返回一個迭代器,指向被刪除元素後面的元素。
除此之外,map 類型還提供了一種額外的 erase 操作其參數是 key_type類型的值,如果擁有該鍵的元素存在,則刪除該元素。對於單詞統計程序,可使用這個版本的 erase 函數來刪除 word_count 中指定的單詞,然後輸出被刪除的單詞:

// erase of a key returns number of elements removed
if (word_count.erase(removal_word))
	cout << "ok: " << removal_word << " removed\n";
else 
	cout << "oops: " << removal_word << " not found!\n";

erase 函數返回被刪除元素的個數。對於 map 容器,該值必然是 0 或 1。如果返回 0,則表示欲刪除的元素在 map 不存在。
表 10.7. 從 map 對象中刪除元素

map查詢操作 操作 作用
m.erase(k) 刪除 m 中鍵爲 k 的元素。返回 size_type 類型的值,表示刪除的元素個數
m.erase( p ) 從 m 中刪除迭代器 p 所指向的元素。p 必須指向 m 中確實存在的元素,而且不能等於 m.end()。返回 void
m.erase(b,e) 從 m 中刪除一段範圍內的元素,該範圍由迭代器對 b 和 e 標記。
b 和 e 必須標記 m 中的一段有效範圍:即 b 和 e 都必須指向m 中的元素或最後一個元素的下一個位置。而且,b 和 e 要麼相等(此時刪除的範圍爲空),要麼 b 所指向的元素必須出現在 e 所指向的元素之前。返回 void 類型

map 對象的迭代遍歷(迭代器)

與其他容器一樣,map 同樣提供 begin 和 end 運算,以生成用於遍歷整個容器的迭代器。例如,可如下將 map 容器 word_count 的內容輸出:

// get iterator positioned on the first element
map<string, int>::const_iterator map_it = word_count.begin();
// for each element in the map
while (map_it != word_count.end()) {
// print the element key, value pairs
	cout << map_it->first << " occurs "<< map_it->second << " times" << endl;
	++map_it; // increment iterator to denote the next element
}

while 循環的條件判斷以及循環體中迭代器的自增都與輸出 vector 或string 容器內容的程序非常相像。首先,初始化 map_it 迭代器,使之指向word_count 的第一元素。只要該迭代器不等於 end 的值,就輸出當前元素並給迭代器加 1。這段程序的循環體要比前面類似的程序更加複雜,原因在於對於
map 的每個元素都必須分別輸出它的鍵和值。
這個單詞統計程序依據字典順序輸出單詞。在使用迭代器遍歷map 容器時,迭代器指向的元素按鍵的升序排列。

“單詞轉換” map 對象

下面的程序說明如何創建、查找和迭代遍歷一個 map 對象,我們將以此結束本節內容。這個程序求解的問題是:給出一個 string 對象,把它轉換爲另一個 string 對象。
本程序的輸入是兩個文件。第一個文件包括了若干單詞對,每對的第一個單詞將出現在輸入的字符串中,而第二個單詞則是用於輸出。本質上,這個文件提供的是單詞轉換的集合——在遇到第一個單詞時,應該將之替換爲第二個單詞。
第二個文件則提供了需要轉換的文本。如果單詞轉換文件的內容是:

'em them
cuz because
gratz grateful
i I
nah no
pos supposed
sez said
tanx thanks
wuz was

而要轉換的文本是:

nah i sez tanx cuz i wuz pos to
not cuz i wuz gratz

則程序將產生如下輸出結果:

no I said thanks because I was supposed to
not because I was grateful

單詞轉換程序

下面給出的解決方案是將單詞轉換文件的內容存儲在一個 map 容器中,將被替換的單詞作爲鍵,而用作替換的單詞則作爲其相應的值。接着讀取輸入,查找輸入的每個單詞是否對應有轉換。若有,則實現轉換,然後輸出其轉換後的單詞,否則,直接輸出原詞。

該程序的 main 函數需要兩個實參(第 7.2.6 節):單詞轉換文件的名字以及需要轉換的文件名。程序執行時,首先檢查實參的個數。第一個實參 argv[0]是命令名,而執行該程序所需要的兩個文件名參數則分別存儲在 argv[1] 及argv[2] 中
如果 argv[1] 的值合法,則調用 open_file(第 8.4.3 節)打開單詞轉換文件。假設 open 操作成功,則讀入“單詞轉換對”。以“轉換對”中的第一個單詞爲鍵,第二個爲值,調用 insert 函數在容器中插入新元素。while 循環結束後,trans_map 容器對象包含了轉換輸入文本所需的數據。而如果該實參有問
題,則拋出異常(第 6.13 節)並結束程序的運行。

接下來,調用 open_file 打開要轉換的文件。第二個 while 循環使用getline 函數行讀入文件。因爲程序每次讀入一行,從而可在輸出文件的相同位置進行換行。然後在內嵌的 while 循環中使用stringstream將每一行中的單詞提取出來。這部分程序與第 8.5 節的程序框架類似。
內層的 while 循環檢查每個單詞,判斷它是否在轉換的 map 中出現。如果在,則從該 map 對象中取出對應的值替代此單詞。最後,無論是否做了轉換,都輸出該單詞。同時,程序使用 bool 值 firstword 判斷是否需要輸出空格。如果當前處理的是這一行的第一個單詞,則無須輸出空格。

/*
* A program to transform words.
* Takes two arguments: The first is name of the word transformation
file
* The second is name of the input to transform
*/
int main(int argc, char **argv)
{
// map to hold the word transformation pairs:
// key is the word to look for in the input; value is word to use in the output
	map<string, string> trans_map;  //用於記錄轉換映射文件的容器
	string key, value; 
	if (argc != 3)  //如果輸入參數不等於三 拋出錯誤
		throw runtime_error("wrong number of arguments");
// open transformation file and check that open succeeded
	ifstream map_file; //文件流
	if (!open_file(map_file, argv[1]))  //打開轉換文件的文件流
		throw runtime_error("no transformation file");
// read the transformation map and build the map
	while (map_file >> key >> value)  //這個輸入厲害啊!
		trans_map.insert(make_pair(key, value));
// ok, now we're ready to do the transformations
// open the input file and check that the open succeeded
	ifstream input;
	if (!open_file(input, argv[2]))
		throw runtime_error("no input file");
	string line; // hold each line from the input
// read the text to transform it a line at a time
	while (getline(input, line))
	 {
		istringstream stream(line); // read the line a word at a time 它的作用是從string對象str中讀取字符。
		//istringstream的構造函數原形如下:istringstream::istringstream(string str);
		string word;
		bool firstword = true; // controls whether a space is printed
		while (stream >> word) 
		{
// ok: the actual mapwork, this part is the heart of the program
			map<string, string>::const_iterator map_it =trans_map.find(word);
// if this word is in the transformation map
			if (map_it != trans_map.end())
// replace it by the transformation value in the map
				word = map_it->second;
			if (firstword)
				firstword = false;
			else
				cout << " "; // print space between words
			cout << word;
		}
		cout << endl; // done with this line of input
	}
return 0;
}

參考資料

【1】C++ Primer 中文版(第四版·特別版)

註解

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