C++容器深入
本文主要討論C++標準庫中的順序容器及相應的容器介面卡,這些內容主要涉及順序容器型別:vector、list、deque,順序容器介面卡型別:stack、queue、priority_queue。
如果文中有錯誤或遺漏之處,敬請指出,謝謝!
概述
標準庫中的容器分為順序容器和關聯容器。順序容器(sequential container)內的元素按其位置儲存和訪問,顧名思義,這些內部元素是順序存放的;順序容器內的元素排列次序與元素值無關,而是由元素新增到容器裡的次序決定。而關聯容器的元素按鍵(key)排序。
容器類共享部分公共介面。標準庫定義的三種順序容器型別:vector、list、deque(double-ended queue的縮寫,發音為“deck”),它們的差別僅在訪問元素的方式,以及新增或刪除元素相關操作的代價。順序容器介面卡包括:stack、queue和priority_queue。容器只定義了少量操作,大多數操作由演算法庫提供。如果兩個容器提供了相同的操作,則它們的介面(函式名和引數個數)應該相同。
vector | 容器,支援快速隨機訪問(連續儲存) |
list | 連結串列,支援快速插入/刪除 |
deque | 雙端佇列,支援隨機訪問(連續儲存),兩端能快速插入和刪除 |
stack | 棧 |
queue | 佇列 |
priority_queue | 優先順序佇列 |
順序容器的定義
順序容器的構造和初始化
下面的順序容器C可以為vector、list和deque型別。
C<T> c; | 建立一個空容器;適用於所有容器。 |
C<T> c2(c); | 建立容器c的副本;適用於所有容器。 |
C<T> c(b, e); | 用迭代器[b, e)範圍內的元素構造容器;適用於所有容器。 |
C<T> c(n); | 建立有n個值為預設的元素的容器;僅適用於順序容器。 |
C<T> c(n, t); | 建立有n個值為t的元素的容器;僅適用於順序容器。 |
注意:在將一個容器複製到另一個容器時,型別必須匹配,容器型別和元素型別必須相同。
容器元素的型別約束
對於所有容器,容器元素型別必須滿足以下約束條件:
1)元素型別必須支援賦值運算;
2)元素型別的物件必須可以複製。
由此知道,所有內建或複合型別都可用做元素型別。引用不支援一般意義的賦值運算,因此沒有元素是引用型別的容器;除了標準庫中的輸入輸出流類和智慧指標(auto_ptr型別)之外,所有其他標準庫型別都是有效的容器元素型別。
如果使用上面順序容器型別中的第四個建構函式,即構造n個有預設值的元素的容器,此時,若元素型別是類型別,則元素型別必須提供預設建構函式。
迭代器及其範圍
下表為迭代器為所有容器型別所提供的運算:
*iter | 返回型別iter所指向的元素的引用 |
iter->mem | 對iter進行解引用,並取得指定成員 |
++iter | 給iter加1,使其指向容器中下一個元素 |
iter++ | |
--iter | 給iter減1,使其指向容器中前一個元素 |
iter-- | |
iter1 == iter2 | 當兩個迭代器指向同一個容器中的同一元素,或者當它們都指向 |
iter1 != iter2 | 同一個容器的超出末端的下一個位置時,兩個迭代器相等。 |
vector和deque容器的迭代器提供了額外的運算:迭代器的算術運算和另一些關係運算,如下表所示:
iter + n | 在迭代器上加(減)整數值,將產生指向容器中前面(後面)第n個元素的迭代器; |
iter - n | 新計算出來的迭代器必須指向容器中的元素或超出容器末端的下一位置。 |
iter1 += iter2 | 複合運算:先加(減),再賦值 |
iter1 -= iter2 | |
iter1 - iter2 | 只適用於vector和deque |
>, >=, <, <= | 比較迭代器的位置關係;只適用於vector和deque |
關係操作符只適用於vector和deque容器,這是因為只有這兩種容器為其元素提供快速、隨機的訪問。它們確保可根據元素位置直接有效地訪問指定的容器元素。這兩種容器都支援通過元素位置實現的隨機訪問,因此它們的迭代器可以有效地實現算術和關係運算。
迭代器範圍:[first, last)是一個左閉合區間,表示範圍從first開始,到last結束,但不包括last。注意:如果first不等於last,則對first反覆做自增運算必須能夠到達last;否則,即last位於first之前,則將發生未定義行為。
迭代器範圍使用左閉合的意義:因為這樣可以統一表示空集,就無需特別處理。
另外,使用迭代器時,要特別留意迭代器的可能的失效問題。
順序容器的操作
容器內定義的類型別名
所有容器內部都提供了下列類型別名:
size_type | 無符號型型,足以儲存此容器型別的最大可能容器長度 |
iterator | 此容器型別的迭代器型別 |
const_iterator | 元素的只讀迭代型別 |
reverse_iterator | 按逆序定址元素的迭代器 |
const_reverse_iterator | 元素的只讀逆序迭代器 |
difference_type | 足夠儲存兩個迭代器差值的有符號整型,可為負數 |
value_type | 元素型別 |
reference | 元素的左值型別,是value_type&的同義詞 |
const_reference | 元素的常量左值型別,等效於const value_type& |
begin和end成員
begin() | 返回指向容器中第一個元素的迭代器 |
end() | 返回指向容器中最後一個元素的下一個位置的迭代器 |
rbegin() | 返回指向容器中最後一個元素的逆序迭代器 |
rend() | 返回指向容器中第一個元素前面的位置的逆序迭代器 |
上面的每個操作都有兩個版本:const成員和非const成員。
新增元素
push_back(t) | 在容器的尾部新增值為t的元素,返回void型別 |
push_front(t) | 在容器的前端新增值為t的元素,返回void型別 |
insert(p, t) | 在迭代器p所指向的元素前面插入值為t的新元素,返回指向新元素的迭代器 |
insert(p, n, t) | 在迭代器p所指向的元素前面插入n個值為t的新元素,返回void型別 |
insert(p, b, e) | 在迭代器p所指向的元素前面插入迭代器範圍[b, e)內的元素,返回void型別 |
注意:往容器中新增元素時,是新增元素的副本。 |
為什麼是在插入點之前而不是之後插入元素呢?原因在於,對於指向容器的最後一個元素的下一個位置的迭代器(即end()),這也是一個合法的迭代器,也應該能夠用它作為插入點。如果在插入點之後插入元素,那麼這個迭代器位置就不能執行插入操作,也就是這個迭代器不能作為插入點。而如果在插入點之前插入元素,那麼所有合法的迭代器都可以作為插入點。所以,選擇了在插入點之前插入元素。
容器物件的比較
所有的容器型別都支援用關係運算符來實現兩個容器的比較。容器的比較是基於容器內元素的比較:
1)如果兩個容器具有相同的長度而且所有元素都相等,那麼這兩個容器相等;否則,它們就不相等。
2)如果兩個容器的長度不相同,但較短的容器中所有元素都等於較長容器中對應的元素,則稱較短的容器小於另一個容器。
3)如果兩個容器都不是對方的子序列,則它們的比較結果取決於所比較的第一個不相等的元素。
容器大小
所有容器都提供了四種與容器大小相關的操作:
size() | 返回容器中的元素個數,返回型別為C::size_type |
max_size() | 返回容器可容納的最多元素個數,返回型別為C::size_type |
empty() | 如果容器為空,則返回true,否則返回false |
resize(n) | 調整容器的長度大小,使其能容納n個元素,新增元素採用值初始化 |
resize(n, t) | 調整容器的長度大小,使其能容納n個元素,所有新增元素值為t |
另外,vector容器還提供了capacity()和reserve(n)兩個函式,以供程式設計師與vector容器記憶體分配的實現部分互動工作。
訪問元素
back() | 返回容器的最後一個元素的引用。如果容器為空,則該操作未定義 |
front() | 返回容器的第一個元素的引用。如果容器為空,則該操作未定義 |
c[n] | 返回下標為n的元素的引用;如果n<0 or n>=size(),則該操作未定義 |
at[n] | 返回下標為n的元素的引用;如果下標無效,則丟擲異常out_of_range異常 (注:只適用於vector和deque容器) |
刪除元素
erase(p) | 刪除迭代器p所指向的元素。返回一個迭代器,它指向被刪除的元素後面的元素。如果p指向容器內最後一個元素,則返回的迭代器指向容器的超出末端的下一個位置;如果p本身就是指向超出末端的下一個位置的迭代器,則該函式未定義 |
erase(b, e) | 刪除[b, e)內的所有元素。返回一個迭代器,它指向被刪除元素段後面的元素。如果e本身就是指向超出末端的下一個位置的迭代器,則返回的迭代器也指向超出末端的下一個位置。 |
clear() | 刪除容器內的所有元素,返回void |
pop_back() | 刪除容器內的最後一個元素,返回void。如果容器為空,則該操作未定義。 |
pop_front() | 刪除容器內的第一個元素,返回void。如果c為空容器,則該操作未定義 (注:只適用於list和deque容器) |
賦值與swap
c1 = c2 | 刪除容器c1的所有元素,然後將c2的元素複製給c1。c1和c2的型別必須相同。 |
c1.swap(c2) | 交換內容:呼叫該函式後,c1中存放的是c2原來的元素,c2中存放的是c1原來的元素。c1和c2的型別必須相同。該函式的執行速度通常要比將c2的元素複製到c1的操作快。 |
c.assign(b, e) | 重新設定c的元素:將迭代器b和e標記的範圍內所有的元素複製到c中。b和e必須不是指向c中元素的迭代器。 |
c.assign(n, t) | 將容器c重新設定為儲存n個值為t的元素。 |
注意:assign操作首先刪除容器內所有的元素,再將引數所指定的新元素插入到容器中。
swap操作不會刪除或插入任何元素,而且保證在常量時間內實現交換。由於容器內沒有移動任何元素,因此迭代器不會失效。但要注意這些迭代器指向了另一個容器中的元素。
容器的選用
vector和deque容器提供了對元素的快速訪問,但付出的代價是,在容器的任意位置插入或刪除元素,比在容器尾部插入和刪除的開銷更大,因為要保證其連續儲存,需要移動元素;list型別在任何位置都能快速插入和刪除,因為不需要保證連續儲存,但付出的代價是元素的隨機訪問開銷較大。特徵如下:
1)與vector容器一樣,在deque容器的中間insert或erase元素效率比較低;
2)不同於vector容器,deque容器提供高效地在其首部實現insert和erase的操作,就像在尾部一樣;
3)與vector容器一樣而不同於list容器的是,deque容器支援對所有元素的隨機訪問。
4)在deque容器首部或尾部刪除元素則只會使指向被刪除元素的迭代器失效。在deque容器的任何其他位置的插入和刪除操作將使指向該容器元素的所有迭代器都失效。
一些容器選用法則:
1)如果程式要求隨機訪問元素,則應使用vector或deque容器;
2)如果程式必須在容器的中間位置插入或刪除元素,則應採用list容器;
3)如果程式不是在容器的中間位置,而是在容器首部或尾部插入或刪除元素,則應採用deque容器;
4)如果只需要在讀取輸入時在容器的中間位置插入元素,然後需要隨機訪問元素,則可以在輸入時將元素讀入到一個list容器中,然後對容器排序,再將排序後的list容器複製到vector容器中。
5)如果程式既需要隨機訪問,又需要在容器的中間位置插入或刪除元素,此時應當權衡哪種操作的影響較大,從而決定選擇list容器還是vector或deque容器。注:此時若選擇使用vector或deque容器,可以考慮只使用它們和list容器所共有的操作,比如使用迭代器而不是下標,避免隨機訪問元素等,這樣在必要時,可以很方便地將程式改寫為使用list容器。
容器介面卡
介面卡(adaptor)是標準庫中通用的概念,包括容器介面卡、迭代器介面卡和函式介面卡。本質上,介面卡是使一事物的行為類似於另一事物的行為的一種機制。容器介面卡讓一種已存在的容器型別採用另一種不同的抽象型別的工作方式實現,只是發生了介面轉換而已。
標準庫提供了三種順序容器介面卡:queue, priority_queue和stack。
所有介面卡都定義了兩個建構函式:預設建構函式用於建立空物件,而帶一個容器引數的建構函式將引數容器的副本作為其基礎值。
預設的stack和queue都基於deque容器實現,而priority_queue則在vector容器上實現。在建立介面卡時,通過將一個順序容器指定為介面卡的第二個型別引數,可覆蓋其關聯的基礎容器型別。例如:
stack<int, vector<int> > int_stack; // 此時,int-stack棧是基於vector實現
對於給定的介面卡,其關聯的容器必須滿足一定的約束條件。stack介面卡所關聯的基本容器可以是任意一種順序容器型別,因為這些容器型別都提供了push_back、pop_back和back操作;queue介面卡要求其關聯的基礎容器必須提供pop_front操作,因此其不能建立在vector容器上;priority_queue介面卡要求提供隨機訪問功能,因此不能建立在list容器上。
兩個相同型別的介面卡可以做==, !=, <, >, <=, >=這些關係運算,只要其基本元素型別支援==和<兩個操作即可。這與容器大小比較原則一致。
棧
s.empty() | 如果棧為這人,則true;否則返回false |
s.size() | 返回棧中元素的個數 |
s.pop() | 刪除棧頂元素,但不返回其值 |
s.top() | 返回棧頂元素的值,但不刪除該元素 |
s.push(item) | 在棧項壓入新元素 |
佇列和優先順序佇列
標準庫佇列使用了先進先出(FIFO)的儲存和檢索策略,進入佇列的元素被放置在尾部,下一個被取出的元素則取自佇列的首部。
priority_queue預設使用元素型別的 < 操作符來確定它們之間的優先順序關係,使用者也可以定義自己的優先順序關係。在優先順序佇列中,新元素被放置在比它優先順序低的元素的前面。
q.empty() | 如果佇列為空,則返回true;否則返回false |
q.size() | 返回佇列中元素的個數 |
q.pop() | 刪除隊首元素,但不返回其值 |
q.front() | 返回隊首元素的值,但不刪除該元素 (注:該操作只適用於佇列) |
q.back() | 返回隊尾元素的值,但不刪除該元素 (注:該操作只適用於佇列) |
q.top() | 返回具有最高優先順序的元素值,但不刪除該元素 |
q.push(item) | 對於queue,在隊尾壓入一個新元素; 對於priority_queue,在基於優先順序的適當位置插入新元素 |
關聯容器:
關聯容器(Associative Container)與順序容器(Sequential Container)的本質區別在於:關聯容器是通過鍵(key)儲存和讀取元素的,而順序容器則通過元素在容器中的位置順序儲存和訪問元素。
關聯容器支援通過鍵來高效地查詢和讀取元素,兩個基本的關聯容器是map和set。map的元素是“鍵-值”對的二元組形式:鍵用作元素在map中的索引,而值則表示所儲存和讀取的資料。set僅包含一個鍵,並有效地支援關於某個鍵是否存在的查詢。set和map型別的物件所包含的元素都具有不同的鍵。如果需要一個鍵對應多個例項,則需要使用multimap或multiset型別。這兩種型別允許多個元素擁有相同的鍵。
map | 關聯陣列:元素通過鍵來儲存和讀取 |
set | 大小可變的集合,支援通過鍵實現的快速讀取 |
multimap | 支援同一個鍵多次出現的map型別 |
multiset | 支援同一個鍵多次出現的set型別 |
pair型別
pair模板類用來繫結兩個物件為一個新的物件,該型別在<utility>標頭檔案中定義。pair型別提供的操作如下表:
pair<T1, T2> p1; | 建立一個空的pair物件,它的兩個元素分別是T1和T2型別,採用值初始化 |
pair<T1, T2> p1(v1, v2); | 建立一個pair物件,它的兩個元素分別是T1和T2型別,其中first成員初始化為v1,second成員初始化為v2 |
make_pair(v1, v2) | 以v1和v2值建立一個新的pair物件,其元素型別分別是v1和v2的型別 |
p1 < p2 | 字典次序:如果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的(公有)資料成員 |
關聯容器
關聯容器共享大部分順序容器的操作,但不提供front, push_front, back, push_back以及pop_back操作。
具體而言,有順序容器中的:前三種建構函式;關係運算;begin, end, rbegin和rend操作;類型別名;swap和賦值操作,但關聯容器不提供assign函式;clear和erase函式,但erase函式返回void型別;關於容器大小的操作,但resize函式不能用於關聯容器。
map型別
map型別定義在標頭檔案<map>中。map是鍵-值對的集合,通常看作關聯陣列:可使用鍵作為下標來獲取一個值。map類定義內部定義的型別有key_type, mapped_type, value_type,如下表所示:
map<K, V>::key_type | 在map容器內,用做索引的鍵的型別 |
map<K, V>::mapped_type | 在map容器中,鍵所關聯的值的型別 |
map<K, V>::value_type | map的值型別:一個pair型別,它的first元素具有 const map<K, V>::key_type型別,而second元素 則為map<K, V>::mapped_type型別 |
注意:map的元素型別為pair型別,且鍵成員不可修改。其它類型別名與順序容器一樣。
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> |
鍵型別的約束
在使用關聯容器時,它的鍵不但有一個型別,而且還有一個相關的比較函式。預設情況下,標準庫使用鍵型別定義的 < 操作符來實現鍵的比較。這個比較函式必須滿足:當一個鍵和自身比較時,結果必定是false;當兩個鍵之間都不存在“小於”關係時,則容器將之視為相同的鍵。也就是說,map內的元素按鍵值升序排列。
operator[]
A::reference operator[](const Key& key); |
[]操作符返回鍵key所關聯的值的引用;如果該鍵key不存在,則向map物件新增一個新的元素,元素的鍵為key,所關聯的值採用值初始化。(要特別留意這個副作用) |
注:map下標操作符返回的型別(mapped_type&)與對map迭代器進行解引用獲得的型別(value_type)不相同。
例如:
map <string, int> wordCount; // empty map
word_count["Hello"] = 1;
上面的程式碼首先建立一個空的map物件,然後執行下列步驟:
1)在wordCount中查詢鍵為“Hello”的元素,沒有找到;
2)將一個新的鍵-值對插入到wordCount中,其中,鍵為“Hello”,值為0
3)讀取新插入的鍵-值對的值,並將它的值賦為1。
應用例項,下面的程式用來統計一篇英文文章中單詞出現的頻率:
|
map::insert
m.insert(e) | e是一個用在m上的value_type型別的值,如果鍵(e.first)不在m中,則插入e到m中;如果鍵已經在m中存在,則保持m不變。 該函式返回一個pair型別物件,如果發生了插入動作,則返回pair(it, true);否則返回pair(it, false)。其中,it是指向鍵為e.first那個元素的迭代器。 |
m.insert(beg, end) | beg和end是標記元素範圍的迭代器,其中的元素必須為value_type型別的鍵-值對。對於該範圍內的所有元素,如果它的鍵在m中不存在,則將該鍵及其關聯的值插入到m。返回void型別。 |
m.insert(iter, e) | insert(e),並以iter為起點搜尋新元素的位置。返回一個迭代器,指向m中鍵為e.first的元素。 |
注:當需要插入一個map元素時,一是可以用map::value_type來構造一個pair物件,另外,也可以用make_pair來構造這個物件。
查詢元素
m.count(k) | 返回m中k的出現次數(0或1) |
m.find(k) | 如果容器中存在鍵為k的元素,則返回指向該元素的迭代器。 如果不存在,則返回end()值。 |
刪除元素
m.erase(k) | 刪除m中鍵為k的元素,返回size_type型別的值,表示刪除的元素個數(0或1) |
m.erase(p) | 從m中刪除迭代器p所指向的元素。p必須指向m中確實存在的元素,而且不能等於e.end()。返回void型別 |
m.erase(b, e) | 從m中刪除[b, e)範圍內的元素,返回void型別 |
set型別
set型別定義於<set>標頭檔案中。set容器支援大部分map容器的操作,如:建構函式;insert操作;count和find操作;erase操作。兩個例外情況是:set不支援下標操作符,而且沒有定義mapped_type型別。與map一樣,set容器儲存的鍵也必須是唯一的,而且不能修改。
multimap和multiset型別
map和set容器中,一個鍵只能對應一個例項。而multiset和multimap型別則允許一個鍵對應多個例項。
multimap和multiset所支援的操作分別與map和set的操作相同,只有一個例外:multimap不支援下標運算。為了順序一個鍵可以對應多個值這一特性,map和mulitmap,或set和multiset中相同的操作都以不同的方式做出了一定的修改。
元素的新增和刪除
map和set容器中的insert和erase操作同樣適用於multimap和multiset容器,實現元素的新增和刪除。
由於鍵不要求是唯一的,因此每次呼叫insert總會新增一個元素。
而帶有一個鍵引數的erase將刪除擁有該鍵的所有元素,並返回刪除元素的個數;而帶有一個或一對迭代器引數的erase版本只刪除指定的元素,並返回void型別。
查詢元素
在map和set容器中,元素是有序儲存的(升序),同樣multimap和multiset也一樣。因此,在multimap和multiset容器中,如果某個鍵對應多個例項,則這些例項在容器中將相鄰存放,即迭代遍歷時,可保證依次返回特定鍵所關聯的所有元素。
要查詢特定鍵所有相關聯的值,可以有下面三種方法:
1)配合使用find和count來查詢:count函式求出某鍵出現的次數,而find操作返回指向第一個鍵的例項的迭代器。
2)使用lower_bound和upper_bound函式:這兩個函式常用於multimap和multiset,但也可以用於map和set容器。所有這些操作都需要傳遞一個鍵,並返回一個迭代器。
m.lower_bound(k) | 返回一個迭代器,指向鍵不小於k的第一個元素 |
m.upper_bound(k) | 返回一個迭代器,指向鍵大於k的第一個元素 |
m.equal_range(k) | 返回一個迭代器的pair物件;它的first成員等價於 m.lower_bound(k),而second成員則等價於 m.upper_bound(k) |
注意:形成的有效區間是[lower_bound(k), upper_bound(i)),是個半開半閉區間。
lower_bound返回的迭代器不一定指向擁有特定鍵的元素。如果該鍵不在容器中,則lower_bound返回在保持容器元素順序的前提下該鍵應被插入的第一個位置。
若鍵不存在,返回的迭代器相同。
3)使用equal_range,其實質跟法2)相同。