1. 程式人生 > 其它 >04 | 容器彙編 I:比較簡單的若干容器

04 | 容器彙編 I:比較簡單的若干容器

string


string 一般並不被認為是一個 C++ 的容器。但鑑於其和容器有很多共同點,我們先拿 string 類來開說。
string 是模板 basic_string 對於 char 型別的特化,可以認為是一個只存放字元 char 型別資料的容器。“真正”的容器類與 string 的最大不同點是裡面可以存放任意型別的物件。
跟其他大部分容器一樣, string 具有下列成員函式:

  • begin 可以得到物件起始點
  • end 可以得到物件的結束點
  • empty 可以得到容器是否為空
  • size 可以得到容器的大小
  • swap 可以和另外一個容器交換其內容

(對於不那麼熟悉容器的人,需要知道 C++ 的 begin 和 end 是半開半閉區間:在容器非空時,begin 指向第一個元素,而 end 指向最後一個元素後面的位置;在容器為空時,begin 等於 end。在 string 的情況下,由於考慮到和 C 字串的相容,end 指向代表字串結尾的 \0 字元。)

string 當然是為了存放字串。和簡單的 C 字串不同

  • string 負責自動維護字串的生命週期
  • string 支援字串的拼接操作(如之前說過的 + 和 +=)
  • string 支援字串的查詢操作(如 find 和 rfind)
  • string 支援從 istream 安全地讀入字串(使用 getline)
  • string 支援給期待 const char* 的介面傳遞字串內容(使用 c_str)
  • string 支援到數字的互轉(stoi 系列函式和 to_string)

如果實現較為複雜、希望使用 string 的成員函式的話,那就應該考慮下面的策略

  • 如果不修改字串的內容,使用 const string& 或 C++17 的 string_view 作為引數型別。後者是最理想的情況,因為即使在只有 C 字串的情況,也不會引發不必要的記憶體複製。
  • 如果需要在函式內修改字串內容、但不影響呼叫者的該字串,使用 string 作為引數型別(自動拷貝)。
  • 如果需要改變呼叫者的字串內容,使用 string& 作為引數型別(通常不推薦)。

string name;
cout << "What's your name? ";
getline(cin, name);
cout << "Nice to meet you, " << name
     << "!\n";

vector


除了容器類的共同點,vector 允許下面的操作(不完全列表)

  • 可以使用中括號的下標來訪問其成員(同 string)
  • 可以使用 data 來獲得指向其內容的裸指標(同 string)
  • 可以使用 capacity 來獲得當前分配的儲存空間的大小,以元素數量計(同 string)
  • 可以使用 reserve 來改變所需的儲存空間的大小,成功後 capacity() 會改變(同 string)
  • 可以使用 resize 來改變其大小,成功後 size() 會改變(同 string)
  • 可以使用 pop_back 來刪除最後一個元素(同 string)
  • 可以使用 push_back 在尾部插入一個元素(同 string)
  • 可以使用 insert 在指定位置前插入一個元素(同 string)
  • 可以使用 erase 在指定位置刪除一個元素(同 string)
  • 可以使用 emplace 在指定位置構造一個元素
  • 可以使用 emplace_back 在尾部新構造一個元素

當 push_back、insert、reserve、resize 等函式導致記憶體重分配時,或當 insert、erase 導致元素位置移動時,vector 會試圖把元素“移動”到新的記憶體區域。vector 通常保證強異常安全性,如果元素型別沒有提供一個保證不拋異常的移動建構函式,vector 通常會使用拷貝建構函式。因此,對於拷貝代價較高的自定義元素型別,我們應當定義移動建構函式,並標其為 noexcept,或只在容器中放置物件的智慧指標。這就是為什麼我之前需要在 smart_ptr 的實現中標上 noexcept 的原因。

C++11 開始提供的 emplace… 系列函式是為了提升容器的效能而設計的。

deque


deque 不提供 data、capacity 和 reserve 成員函式。

如果你需要一個經常在頭尾增刪元素的容器,那 deque 會是個合適的選擇。

list



需要指出的是,雖然 list 提供了任意位置插入新元素的靈活性,但由於每個元素的記憶體空間都是單獨分配、不連續,它的遍歷效能比 vector 和 deque 都要低。這在很大程度上抵消了它在插入和刪除操作時不需要移動元素的理論效能優勢。如果你不太需要遍歷容器、又需要在中間頻繁插入或刪除元素,可以考慮使用 list。

另外一個需要注意的地方是,因為某些標準演算法在 list 上會導致問題,list 提供了成員函式作為替代,包括下面幾個:

  • merge
  • remove
  • remove_if
  • reverse
  • sort
  • unique

forward_list


大部分 C++ 容器都支援 insert 成員函式,語義是從指定的位置之前插入一個元素。對於 forward_list,這不是一件容易做到的事情(想一想,為什麼?)。標準庫提供了一個 insert_after 作為替代。此外,它跟 list 相比還缺了下面這些成員函式:

為什麼會需要這麼一個閹割版的 list 呢?原因是,在元素大小較小的情況下,forward_list 能節約的記憶體是非常可觀的;在列表不長的情況下,不能反向查詢也不是個大問題。提高記憶體利用率,往往就能提高程式效能,更不用說在記憶體可能不足時的情況了。

queue


依賴於某個現有的容器,因而被稱為容器介面卡(container adaptor)。
它的實際記憶體佈局當然是隨底層的容器而定的。從概念上講,它的結構可如下所示:

stack


類似地,棧 stack 是後進先出(LIFO)的資料結構。

  • 用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_back;沒有其他的 push_…、pop_…、emplace…、insert、erase 函式