1. 程式人生 > >C++ 之高效使用STL (STL 容器的選擇)

C++ 之高效使用STL (STL 容器的選擇)

 你知道C++中有很多你可以支配的容器,但是你意識到有多少嗎?要確定你沒有忽略你的選項,這裡有一個快速回顧。
  • 標準STL序列容器:vector、string、deque和list。
  • 標準STL關聯容器:set、multiset、map和multimap。
  • 非標準序列容器slist和rope。slist是一個單向連結串列,rope本質上是一個重型字串。(“繩子(rope)”是重型的“線(string)”。明白了嗎?)你可以找到一個關於這些非標準(但常見的)容器的概覽在條款50
  • 非標準關聯容器hash_set、hash_multiset、hash_map和hash_multimap。我在條款25檢驗了這些可以廣泛獲得的基於散列表的容器和標準關聯容器的不同點。
  • vector<char>可以作為string的替代品。
    條款13描述了這個替代品可能會有意義的情況。
  • vector作為標準關聯容器的替代品。就像條款23所說的,有時候vector可以在時間和空間上都表現得比標準關聯容器好。
  • 幾種標準非STL容器,包括陣列、bitset、valarray、stack、queue和priority_queue。因為它們是非STL容器,所以在本書中關於它們我說得很少,雖然條款16提到了陣列比STL容器更有優勢的一種情況,而條款18揭示了為什麼bitset可能比vector<bool>要好。值得注意的是,陣列可以和STL演算法配合,因為指標可以當作陣列的迭代器使用。

這是所有的選項,而且可以考慮的範圍和可以在它們之間的選擇一樣豐富。不走運的是,STL的大多數討論只限於容器世界的一個很窄的視野,忽略了很多關於選擇適當容器的問題。就連標準都介入了這個行動,提供了以下的在vector、deque和list之間作選擇的指導方案:

vector、list和deque提供給程式設計師不同的複雜度,因此應該這麼用:vector是一種可以預設使用的序列型別,當很頻繁地對序列中部進行插入和刪除時應該用list,當大部分插入和刪除發生在序列的頭或尾時可以選擇deque這種資料結構。

如果你主要關心的是演算法複雜度,我想這個方案是有理由的建議,但需要關心更多東西。

現在,我們要檢查一些可以補充演算法複雜度的重要的容器相關問題,但首先我需要介紹一種STL容器的分類方法,它被討論的次數並不像它應該的那樣多。那是連續記憶體容器和基於節點的容器的區別。

連續記憶體容器(也叫做基於陣列的容器)在一個或多個(動態分配)的記憶體塊中儲存它們的元素。如果一個新元素被查入或者已存元素被刪除,其他在同一個記憶體塊的元素就必須向上或者向下移動來為新元素提供空間或者填充原來被刪除的元素所佔的空間。這種移動影響了效率(參見條款5

14)和異常安全(就像我們將會看到的)。標準的連續記憶體容器是vector、string和deque。非標準的rope也是連續記憶體容器。

基於節點的容器在每個記憶體塊(動態分配)中只儲存一個元素。容器元素的插入或刪除隻影響指向節點的指標,而不是節點自己的內容。所以當有東西插入或刪除時,元素值不需要移動。表現為連結串列的容器——比如list和slist——是基於節點的,所有的標準關聯容器也是(它們的典型實現是平衡樹)。非標準的雜湊容器使用不同的基於節點的實現,就像我們將會在條款25中看到的。

利用這個不恰當的術語,我們已經準備好描述一些大多數關於在容器間選擇的問題。在這個討論中,我略過考慮非STL類容器(比如,陣列、bitset等),因為畢竟這是本關於STL的書。

  • 你需要“可以在容器的任意位置插入一個新元素”的能力嗎?如果是,你需要序列容器,關聯容器做不到。
  • 你關心元素在容器中的順序嗎?如果不,雜湊容器就是可行的選擇。否則,你要避免使用雜湊容器。
  • 必須使用標準C++中的容器嗎?如果是,就可以除去雜湊容器、slist和rope。
  • 你需要哪一類迭代器?如果必須是隨機訪問迭代器,在技術上你就只能限於vector、deque和string,但你也可能會考慮rope(關於rope的更多資訊在條款50)。如果需要雙向迭代器,你就用不了slist(參見條款50)和雜湊容器的一般實現(參見條款25)。
  • 當插入或者刪除資料時,是否非常在意容器內現有元素的移動?如果是,你就必須放棄連續記憶體容器(參見條款5)。
  • 容器中的資料的記憶體佈局需要相容C嗎?如果是,你就只能用vector(參見條款16)。
  • 查詢速度很重要嗎?如果是,你就應該看看雜湊容器(參見條款25),排序的vector(參見條款23)和標準的關聯容器——大概是這個順序。
  • 你介意如果容器的底層使用了引用計數嗎?如果是,你就得避開string,因為很多string的實現是用引用計數(參見條款13)。你也不能用rope,因為權威的rope實現是基於引用計數的(參見條款50)。於是你得重新稽核你的string,你可以考慮使用vector<char>。
  • 你需要插入和刪除的事務性語義嗎?也就是說,你需要有可靠地回退插入和刪除的能力嗎?如果是,你就需要使用基於節點的容器。如果你需要多元素插入(比如,以範圍的方式——參見條款5)的事務性語義,你就應該選擇list,因為list是唯一提供多元素插入事務性語義的標準容器。事務性語義對於有興趣寫異常安全程式碼的程式設計師來說非常重要。(事務性語義也可以在連續記憶體容器上實現,但會有一個性能開銷,而且程式碼不那麼直觀。要了解這方面的知識,請參考Sutter的《Exceptional C++》的條款17[8]。)
  • 你要把迭代器、指標和引用的失效次數減到最少嗎?如果是,你就應該使用基於節點的容器,因為在這些容器上進行插入和刪除不會使迭代器、指標和引用失效(除非它們指向你刪除的元素)。一般來說,在連續記憶體容器上插入和刪除會使所有指向容器的迭代器、指標和引用失效。
  • 你需要具有有以下特性的序列容器嗎:1)可以使用隨機訪問迭代器;2)只要沒有刪除而且插入只發生在容器結尾,指標和引用的資料就不會失效?這個一個非常特殊的情況,但如果你遇到這種情況,deque就是你夢想的容器。(有趣的是,當插入只在容器結尾時,deque的迭代器也可能會失效,deque是唯一一個“在迭代器失效時不會使它的指標和引用失效”的標準STL容器。)

這些問題幾乎不是事情的完結。比如,它們沒有關注不同的容器型別使用不同的記憶體配置策略(條款1014討論了這些策略的一些方面)。但是,它們已經足夠是你信服了,除非你對元素順序、標準的一致性、迭代器能力、記憶體佈局和C的相容性、查詢速度、因為引用計數造成的行為不規則、事務性語義的輕鬆實現和迭代器失效的條件沒興趣,你得在容器操作的演算法複雜度上花更多的考慮時間。當然這樣的複雜度是重要的,但這離整個故事很遠。

當面對容器時,STL給了你很多選項。如果你的視線超越了STL的範圍,那就會有更多的選項。在選擇一個容器前,要保證考慮了所有你的選項。一個“預設容器”?我不這麼認為。