C++ 順序容器基礎知識總結

2021-03-02 C語言與C++編程

作者:melonstreet 

連結:https://www.cnblogs.com/QG-whz/p/5152963.html

閱讀目錄0、前言

本文簡單地總結了STL的順序容器的知識點。文中並不涉及具體的實現技巧,對於細節的東西也沒有提及。一來不同的標準庫有著不同的實現,二來關於具體實現《STL源碼剖析》已經展示得全面細緻。所以本文僅僅是對容器基礎知識的歸納。至於容器提供的接口與使用實例,建議查取官方文檔。文章難免有錯漏,希望指出。

1、容器概論

容器,置物之所也。像桶可裝水,碗可盛湯,C++的容器,可以存儲對象。容器有多種,用來處理不同的元素操作訴求。按照元素存儲到容器中以及訪問方式的差異,容器分為順序容器與關聯容器。順序容器也稱為序列式容器。序列式容器按元素插入的順序存儲元素,這些元素可以進行排序,但未必是有序的。C++本身內置了一個序列式容器array(數組),STL另外提供了vector,list,forward_list,deque,stack,queue,priority-queue,string等等序列式容器。所有的容器都是基於模板實現的,因為容器必須保證能裝得下各種各樣的類型。其中,stack,queue都是基於deque來實現的,priority-queue基於heap來實現,從技術上來說它們屬於容器適配器(adapter)。其中array與forward_list是C++11添加的新容器類型。

2、std::array2.1.底層數據結構

array的底層數據結構是固定數組。與C-style的數組類似,它的大小在定義後就不能被改變。由於array具有固定的大小,它不支持添加和刪除元素或改變容器大小等其他容器擁有的操作。在定義一個array容器的時候必須指定大小:

Defined in header <array>
template<
    class T,
    std::size_t N
> struct array;


2.2.內存分配策略

在內存分配策略上,array也與C-style數組類似。編譯器在哪裡為array分配內存,取決於array定義的位置和方式。

若作為函數的局部對象,則將從棧上獲得內存,與之對比是的vector,vector底層數據結構是動態數組,從自由存儲區上分配內存:

若使用new操作符分配內存,則是在自由存儲區上分配內存。

若作為全局變量或局部靜態變量,則是在全局/靜態存儲區上分配的內存。

例如,在函數定義的array局部對象在棧上分配內存,與此對比的是vector,它底層數據結構為動態數組,因此在自由存儲區上分配內存:

#include <array>
#include <vector>
#include <iostream>

using namespace std;

int main() {
    int stack_var;
    array<int, 5> a;
    vector<int> b(5);

    cout << "stack area: 0x" << hex << (uintptr_t)&stack_var << endl;
    cout << "a[3] address: 0x" << hex << (uintptr_t)&a[3] << endl;
    cout << "b[3] address: 0x" << hex << (uintptr_t)&b[3] << endl;

    getchar();
    return 0;
}


結果:

2.3.array的優勢在哪

array比數組更安全。它提供了opeartor[]與at()成員函數,後者將進行數組越界檢查。

與其他容器相似,array也有自己的迭代器,因此array能夠更好地與標準算法庫結合起來。

通過array::swap函數,可以實現線性時間內的兩個數組內容的交換。

另外,不像C-style數組,array容器類型的名稱不會自動轉換為指針。對於C++程式設計師來說,array要比C-style數組更好用。

3、forward_list3.1.底層數據結構

forward_list的底層數據結構為單向鍊表。如C++標準所講,forward_list容器支持前向遍曆元素序列,允許常數時間內在任意位置的插入或刪除操作並進行自動的內存管理。與list的主要區別是forward_list沒有反方向的迭代器,不過也正因如此,forward_list的每個節點都節省了迭代器大小的開銷,在元素眾多的時候,將比list消耗少得多的內存。

受單向鍊表這種特殊結構的影響,forward_list在很多地方表現得和其他容器不同:

3.2.forward_list特殊之一:forward_list不提供返回其大小的操作。

在所有已知的STL容器中,forward_list是唯一一個不提供size()的容器。不提供的原因在於計算一個forward_list的長度需要線性的時間,庫用戶有時無法忍受這樣的時間開銷。其他容器提供的size()操作皆可以在常數時間內完成(在C++98時,list也是線性時間)。為了節省內存,forward_list甚至不跟蹤序列的長度,要想獲得某個forward_list對象的長度,用戶需要通過distance()來計算。這帶來了一些不便,但使得用戶遠離了size()帶來的高消耗。每個容器類型都有三個與大小相關的操作:

max_size(),empty(),size(),而forward_list只提供了前兩個。

int main()
{
    forward_list<int> flist;
    for (int i = 0; i < 10; i++)
    {
        flist.push_front(i);
    }
    std::cout << std::distance(flist.begin(), flist.end()); 
    getchar();
}


3.3.forward_list特殊之二:forward_list是唯一一個在給定位置之後插入新元素的容器。

為此,forward_list提供了如下的插入接口:

insert_after在給定位置之後插入新元素emplace_after在給定位置之後構造新元素erase_after刪除給定位置之後的元素splice_after將另一個forward_list的元素移動到本forward_list的指定位置之後

其他所有STL容器都是在指定位置之前插入元素(除了std::array,它不允許插入)。forward_list的這種特殊處理,還是出於效率的考慮。對於單鍊表我們應該很熟悉,為了在某個指定節點之前插入插入節點,我們必須改變插入位置的前一個節點的指向。換句話說,為了在指定節點之前插入新元素,我們必須要先獲得插入位置前一個位置的節點,為了獲取前面這個節點,需要線性的操作時間。


而如果我們是在指定位置之後插入新元素,則無需線性時間的查找操作,這樣可實現常數時間的插入:


同樣的,處於性能的考慮,forward_list沒有提供在尾部進行操作的接口,包括push_back(),pop_back()和emplace_back(),這些操作對單列表來說都至少要花費O(n)來完成。

3.4.迭代器失效問題

指向被刪除元素的迭代器,在刪除之後失效。

4、list4.1.底層數據結構

list同樣是一個模板類,它底層數據結構為雙向循環鍊表。因此,它支持任意位置常數時間的插入/刪除操作,不支持快速隨機訪問。

4.2、迭代器類型

list的迭代器具備前移、後移的能力,所以list提供的是Bidirectional iterator(雙向迭代器)。由於採用的是雙向迭代器,自然也很方便在指定元素之前插入新節點,所以list很正常地提供了insert()操作與push_back()/pop_back()操作。在C++11中,list新增了三個接口,以支持在指定位置構造對象後插入容器中:

emplace在指定位置之前插入新構造的元素emplace_front在鍊表頭插入新構造的元素emplace_back在鍊表尾插入新構造的元素4.3.內存分配策略

list的空間配置策略,自然是像我們普通雙向鍊表那樣,有多少元素申請多少內存。它不像vactor那樣需要預留空間供新元素的分配,也不會因找不到連續的空間而引起整個容器的內存遷移。

4.4.迭代器失效問題

list 有一個重要性質:插入操作(insert)與接合操作(splice)都不會造成原有的list迭代器失效。這在vector是不成立的,因為vactor的插入可能引起空間的重新配置,導致原來的迭代器全部失效。list的迭代器失效,只會出現在刪除的時候,指向刪除元素的那個迭代器在刪除後失效。

通常來說,forward_list在使用靈活度上比不上list,因為它只能單向迭代元素,且提供的接口沒有list多。然而,在內存的使用上,它是比list佔優勢的。當對內存的要求佔首要位置時,應該選擇forward_list。

5、vector5.1.底層數據結構

vector的底層數據結構是動態數組,因此,vector的數據安排以及操作方式與std::array十很相似,它們間的唯一差別在於對空間的運用靈活性上。array為靜態數組,有著靜態數組最大的缺點:每次只能分配一定大小的存儲空間,當有新元素插入時,要經歷 「找到更大的內存空間」->「把數據複製到新空間」 ->「銷毀舊空間」 三部曲, 對於std::array而言,這種空間管理的任務壓在使用它的用戶身上,用戶必須把握好數據的數量,儘量在第一次分配時就給數據分配合理的空間(這有時很難做到),以防止「三部曲」帶來的代價,而數據溢出也是靜態數組使用者需要注意的問題。而vector用戶不需要親自處理空間運用問題。vector是動態空間,隨著新元素的插入,舊存儲空間不夠用時,vector內部機制會自行擴充空間以容納新元素,當然,這種空間擴充大部分情況下(幾乎是)也逃脫不了「三部曲」,只是不需要用戶自己處理,而且vector處理得更加安全高效。vector的實現技術關鍵就在於對其大小的控制以及重新配置時數據移動效率。

5.2.迭代器類型

對於C_style數組,我們使用普通指針就可以對數組進行各種操作。vector維護的是一個連續線性空間,與數組一樣,所以無論其元素型別為何,普通指針都可以作為vector的迭代器而滿足所有必要的條件。vector所需要的迭代器操作,包括operator*,operator->,operator++,operator--,operator+=,operator-=等,普通指針都具有。因此,普通指針即可滿足vector對迭代器的需求。所以,vector提供了Random Access Iterators。

5.3.內存分配策略

標準庫的實現者使用了這樣的內存分配策略:以最小的代價連續存儲元素。為了使vector容器實現快速的內存分配,其實際分配的容量要比當前所需的空間多一些(預留空間),vector容器預留了這些額外的存儲區用於存放添加的新元素,於是不必為每個新元素進行一次內存分配。當繼續向容器中加入元素導致備用空間被用光(超過了容量 capacity),此時再加入元素時vector的內存管理機制便會擴充容量至兩倍,如果兩倍容量仍不足,就擴張至足夠大的容量。容量擴張必須經歷「重新配置、元素移動、釋放原空間」這個浩大的工程。按照《STL源碼剖析》中提供的vector源碼,vector的內存配置原則為:

當然,vector的每種實現都可以自由地選擇自己的內存分配策略,分配多少內存取決於其實現方式,不同的庫採用不同的分配策略。

5.4.迭代器失效問題

1、vector管理的是連續的內存空間,在容器中插入(或刪除)元素時,插入(或刪除)點後面的所有元素都需要向後(或向前)移動一個位置,指向發生移動的元素的迭代器都失效。這裡以插入操作示例:

2、隨著元素的插入,原來分配的連續內存空間已經不夠且無法在原地拓展新的內存空間,整個容器會被copy到另外一塊內存上,此時指向原來容器元素的所有迭代器通通失效。

3、刪除元素後,指向被刪除元素的迭代器失效,這是顯而易見的。

6、deque6.1.底層數據結構

vector是單向開口的線性連續空間,deque則是一種雙向開口的連續數據空間。所謂的雙向開口,意思是可以在頭尾兩端分別做元素的插入和刪除操作。當然vector也可以在頭尾兩端進行操作,但是其頭部操作效果奇差,所以標準庫沒有為vector提供push_front或pop_front操作。與vector類似,deque支持元素的快速隨機訪問。deque的示意圖如下:

現在問題來了:如果deque以數組來實現,如何做到在頭部的常數時間插入?如果是採用鍊表來實現,又如何做到快速隨機訪問?deque的內部數據結構到底如何?想必你已經猜到了,要實現如上需求,需要由一段一段的連續空間連結起來的數據結構才能滿足。

6.2.內存分配策略

接著上面講。deque由一段一段的連續空間所連結而成,一旦需要在deque的前端或尾端增加新空間,便配置一段定量的連續空間,並將該空間串接在deque的頭部或尾部。deque複雜的迭代器架構,構建出了所有分段連續空間」整體連續「的假象。


既然deque是由一段一段定長的連續空間所構成,就需要有結構來管理這些連續空間。deque採用一塊map(非STL中的map)作為主控,map是一塊小的連續空間,其中每個元素都是指針,指向一塊較大的線性連續空間,稱為緩衝區。而緩衝區才是存儲deque元素的空間主體。示例圖:

map本身也是一塊固定大小的連續空間,當緩衝區數量增多,map容不下更多的指針時,deque會尋找一塊新的空間來作為map。

6.3.deque的迭代器

為了使得這些分段的連續空間看起來像是一個整體,deque的迭代器必須有這樣的能力:它必須能夠指出分段連續空間在哪裡,判斷自己所指的位置是否位於某一個緩衝區的邊緣,如果位於邊緣,則執行operator-- 或operator++時要能夠自動跳到下一個緩衝區。因此,儘管deque的迭代器也是Ramdon Access Iterator 迭代器,但它的實現要比vector的複雜太多。SGI版本的STL deque實現思路可以看侯捷的《STL源碼剖析》。

6.4.迭代器失效問題

在deque容器首部或者尾部插入元素不會使得任何迭代器失效。

在其首部或尾部刪除元素則只會使指向被刪除元素的迭代器失效。

在deque容器的任何其他位置的插入和刪除操作將使指向該容器元素的所有迭代器失效。

7、容器適配器

stack,也稱為棧,是一種先進後出的數據結構。STL中的statck是一種容器適配器。所謂的容器適配器,是以某種容器作為底部容器,在底部容器之上修改接口,形成另一種風貌。stack默認以雙端隊列deque作為底部容器。stack沒有提供迭代器,通過push/pop接口對棧頂元素進行操作。

queue,也稱為隊列,是一種先進先出的數據結構,它同樣也是一種容器適配器。它的底部容器默認為deque。同樣,queue也沒有提供迭代器,通過push向隊尾壓入元素,pop從隊首彈出元素。

priority-queue,優先隊列,是一種擁有權值觀念的隊列,例如在以整數大小作為衡量的權值定義下,priority-queue總是彈出最大的數。priority-queue的底部數據結構默認是max-heap,大頂堆。

8、總結array固定大小的數組支持快速隨機訪問不能添加或刪除元素通常不會發生迭代器失效,除非對象已經被銷毀,則原來的迭代器全部失效vector可動態增長的數組支持快速隨機訪問尾部可高效插入/刪除元素若插入操作引起內存重新分配,則全部迭代器失效;否則插入點/刪除點之後的迭代器失效;list雙向鍊表只支持元素的雙向順序訪問在list的任何位置可高效插入/刪除元素插入操作後指向容器的迭代器有效;刪除操作指向其他位置的迭代器有效deque雙端隊列支持快速隨機訪問首尾可高效插入/刪除元素情況較多,見上面分析forward_list單向鍊表只支持元素的單向順序訪問在鍊表的任何位置可高效插入/刪除元素插入操作後指向容器的迭代器有效;刪除操作指向其他位置的迭代器有效string只存儲字符元素的動態數組支持快速隨機訪問尾部可高效插入/刪除元素若插入操作引起內存重新分配,則全部迭代器失效;否則插入點/刪除點之後的迭代器失效;stack默認deque先進後出,只能訪問棧頂元素----沒有迭代器queue默認deque先進先出,只能訪問隊首元素----沒有迭代器priority-queue默認max-heap先進先出,只能訪問隊首元素----沒有迭代器

注意:

「尾部可高效插入/刪除元素」,意味著在除了尾部之外的其他位置插入/刪除元素是較低效的。

「順序訪問」意味著要訪問某一個元素,必須遍歷其他元素。

迭代器失效意味著指針、引用在同樣的情況下也會失效。

9、一些參考連結http://en.cppreference.com/w/cpp/container/array
https://msdn.microsoft.com/en-us/library/22a9t119.aspx
https://www.ibm.com/developerworks/community/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/new_class_template_of_sequentail_containers_in_c_11_14?lang=en

●編號584,輸入編號直達本文

●輸入m獲取文章目錄

分享C/C++技術文章

相關焦點

  • C++隨機排序容器中的元素
    作者:apocelipes連結:https://www.cnblogs.com/apocelipes/p/10351335.html在各種程序語言中都提供了將容器元素隨機排序的shuffle方法,c++也不例外。
  • 跟我學C++中級篇——STL的學習
    另外在此基礎上,還要提醒同學們的是,除了上面的庫,在各個平臺的開發廠商中,還會針對實際情況,對標準庫進行擴展,這些可以歸納為擴展庫。同時,隨著c++標準的不斷迭代,還推出了很多新的庫,同學們需要不斷的學習跟進,目前最新的c++標準為c++20。
  • C++基礎總結(一):從「hello world」入門C++!
    最近對C++的基礎知識進行了大匯總,當然這是精簡版的,但是篇幅也不少,所以今天先分享一下hello world,建議大家收藏慢慢學習,同時希望對大家的C++學習有所幫助。C++ 是一種靜態類型的、編譯式的、通用的、大小寫敏感的、不規則的程式語言,支持過程化編程、面向對象編程和泛型編程。C++ 被認為是一種中級語言,它綜合了高級語言和低級語言的特點。
  • 淺談C++下STL庫中的容器底層小知識
    前排友情提醒,本文章的讀者需要具備一些C++基礎,不是面向小白的文章,抱歉。
  • json for modern c++的使用
    ————【以下是正文】————   最近學習了json for modern c++的使用,在此總結一些常用功能使用方法。老規矩,還是先簡單介紹一下什麼是json吧。JSON(JavaScript Object Notation) 是一種輕量級的數據交換格式。它基於ECMAScript的一個子集。
  • 簡要記錄丨VSCode 搭建基礎 C/C++ 編譯環境
    作者丨FightingBoom來源丨嵌入式基礎學習教程1 參考資料  謝謝各位前輩的教程幫助,十分感激!2 簡要說明  首先要明白,VSCode 僅僅只是一個文本編輯器而已,類似記事本、 Sublime Text 等,本身不具備編譯器的功能。  但是 VSCode 可以通過安裝各種擴展插件,實現代碼編譯、調試、運行等功能。
  • C++ 優先隊列priority_queue
    這三個參數的含義分別為:數據類型,容器類型和比較函數,實際上優先隊列就是維護了一個裝有 T 類型元素的容器 Container,並在入隊和出隊時對容器內元素使用 Compare 比較函數進行了排序。這3個參數還要滿足一定的要求,並且在使用過程中有些注意事項:1.
  • 玩轉C++容器中的查找功能——自定義對象
    C++標準中std提供了幾種容器,它們包括順序容器,比如vector, list, deque, queue, stack等,關聯容器 ,比如map, set等,其中使用頻率比較高的容器是vecotor向量容器、map鍵值對容器,我們經常會使用這兩個容器來存儲數據,然後根據不同的場景來查找獲取容器內的值
  • c++簡介及順序結構
    c++介紹C++ 是一種靜態類型的、編譯式的、通用的、大小寫敏感的、不規則的程式語言,支持過程化編程、面向對象編程和泛型編程。C++ 被認為是一種中級語言,它綜合了高級語言和低級語言的特點。介紹Dev-C++是一個Windows環境下的一個適合於初學者使用的輕量級 C/C++ 集成開發環境(IDE),實現對c++程序的編輯、編譯、運行和調試等工作。2.2. 調整編輯器
  • 「最佳實踐」C++陷阱與套路
    本文結合號主的工作經驗和學習心得,對C++語言的一些高級特性,做了簡單介紹;對一些常見的誤解,做了解釋澄清;對比較容易犯錯的地方,做了歸納總結;希望藉此能增進大家對C++語言了解,減少編程出錯,提升工作效率。
  • C++ 的門門道道 | 技術頭條 - CSDN
    Rule:C++在不同模塊(源文件)裡定義的全局變量,不保證構造順序;但保證在同一模塊(源文件)裡定義的全局變量,按定義的先後順序構造,按定義的相反次序析構。比較low的處理方式可以把待刪元素放到另一個容器WaitEraseContainer裡保存下來,再走一趟單獨的循環,刪除待刪元素。當然,我們推薦在遍歷的同時刪除,因為這樣效率更高,也顯得行家裡手。
  • 編程基礎,結構(Struct)
    今天開始福哥開始給大家講編程的基礎知識,這個基礎知識是用來提高編程水平的知識,基礎知識每種程式語言的差異會比較大,所以福哥在講解某一種程式語言的某一特性的時候,會標明這種特性針對的是哪一種程式語言,只想學習某一種程式語言的童鞋可以有選擇的學習。
  • c++ 之布爾類型和引用的學習總結!
    2、c++中的三目運算符可以直接返回變量本身,既可以作為右值使用,也可以作為左值來使用。3、c++中的三目運算符可能返回的值中如果有一個是常量值,則不能作為左值進行使用,這點要切記和理解。/a.outa=5,b=6a=6,b=53、特殊的引用:--在c++中可以聲明const引用。
  • ​跟我學C++中級篇——STL的容器vector
    一、順序容器vectorC++程式設計師中,如果用到過STL,那麼一定肯定用過vector,這個是最常見,最初步的一個數據類型。上一篇提到的array遠遠比不上它。畢竟那玩意兒相對vector是很久遠後才提出來的。在這之前,std::vector承擔了多少小菜鳥處理數組各種問題的最優選方法。不用處理內存,可以刪除,任意增加不考慮越界。那簡直是一種最單純質樸的快樂。
  • C 語言會比 C++ 快?
    我們將看一下實現的幾個變化過程,首先是從使用 C ++ 容器和算法的變化開始,這將有助於該算法,然後一次刪除一個 C ++ 特性並測試編譯速度和運行時的性能。如果我們使用此值作為鍵進行排序,那麼排序順序就不會完全按照完整的32位鍵進行排序。然而,在我們的例子中,我們需要排序以便能夠首先更好的基於啟發式算法處理邊壓縮,並且啟發式算法是一個粗略的近似過程,因此我們的排序帶來的額外錯誤並不明顯。這種技術在其他您不一定需要確切順序的領域中非常有用。一次基數排序的好處是它更快(你只需要對數據進行1次排序而不是 3 次!)
  • C /C++知識要點總結
    C C++知識要點總結.png一、 數據類型及運算求補碼原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1原碼轉補碼不考慮符號位補碼轉原碼,符號位不參與運算取反後 + 1 == 取反前 - 1科學計數法表示1.8 * 10^11 --> 1.8E119.34 * 10^-3 --> 9.34E-3相關細節
  • C++機器學習庫介紹
    +11 -lboost_serialization -lshark -lcblas用Shark實現線性回歸初始化階段我們將從包含線性回歸的庫和頭函數開始:#include <bits/stdc++.h> //所有c++標準庫的頭文件#include <shark/
  • c++ fstream + string 處理大數據
    (4)上面兩點算是自己的誤解吧,因為c++裡面也有也有與之對應的fstream類,c++map容器類,詳見c++ map簡介(5)c++裡面也有相對比較成熟的string類,裡面的函數也大部分很靈活,沒有的也可以很容易的實現split,strim等,詳見c++string實現(6)最近從網上,看到了一句很經典的話,c++的風fstream類 + string
  • 那些容易犯錯的c++保留字
    本文首發 | 公眾號:lunvey目前正在學習vc++6.0開發,而這裡面使用的是c++98標準。
  • 跟我學C++中級篇——STL中的字符串
    它不算是STL的容器,它只是一個類。+》和網上c++大牛陳碩的相關資料完善的。這裡仍然提醒注意的是,新標準在快速迭代,要注意更新標準的知識。比如在c++11中增加了大量的數字和字符串的直接轉換操作,增加類似棧操作的POP和相關的front等。具體的細節這裡就不再一一展開,用到哪個後,直接查詢一下相關的幫助或者c++開發網站,即可正確使用。四、總結總體來說,string這個類在c++的開發應用中是非常普遍的,不管是小白和老鳥,都會經常用到。