【C++後臺開發面試】STL六大元件(一)
1.六大元件及其關係
- Container(容器) 各種基本資料結構
- Adapter(介面卡) 可改變containers、Iterators或Function object介面的一種元件
- Algorithm(演算法) 各種基本演算法如sort、search…等
- Iterator(迭代器) 連線containers和algorithms
- Function object(函式物件)
- Allocator(分配器)
(一)容器-Container
容器類是容納、包含一組元素或元素集合的物件
七種基本容器:向量(vector)、雙端佇列(deque)、列表(list)、集合(set)、多重集合(multiset)、對映(map)和多重對映(multimap)
序列式容器
- 序列式容器Sequence
containers,其中每個元素均有固定位置——取決於插入時機和地點,和元素值無關。(vector、deque、list)
list:雙向連結串列:
- 基於雙向環狀連結串列實現;尾部有空白節點(為滿足左閉右開);內部有一個 last 迭代器指向尾端空白節點(其 next 為 begin 迭代器)。
- 插入和刪除快,但是隨機訪問比較慢,時間複雜度為O(1);
- 需要頻繁進行插入或刪除操作且不需要過多地在序列內部進行長距離跳轉,應該選擇list
- List 不能使用演算法 sort(只接受隨機存取迭代器),值接受雙向迭代器,不支援隨機存取迭代器,它有自己內建的 sort。
vector:
- 動態陣列,基於陣列的實現,
- 從後面插入和刪除元素,push_back,pop_back,隨機訪問快 ,插入和刪除慢,因為會造成記憶體塊的拷貝,時間複雜度為O(n)
- 迭代器在增加資料,記憶體擴容時必定失效,因為記憶體地址都變了,刪除資料時看編譯器情況,vs會失效。
- 維護三個迭代器:start,finish,end_of_storage;
- 記憶體分配:
- vector動態陣列只會增加記憶體,不會刪除空間,當空間不夠時會自動申請另一片更大的空間,然後把原有資料拷貝過去,並刪除原來的空間的資料,但是儲存空間不會釋放,要等到vector呼叫解構函式的時候才會釋放空間。
- 擴容原則是:
- VS2015 下配對原始碼 每次擴容50%,原來空間大小9,擴容之後9+9/2=13;
- Ubuntu 下原始碼是按每次增長兩倍算;
- 若原本空間為0,第一次配置擴容為1,否則按1.5或2倍來來算
- 強制釋放記憶體:.clear()函式只會清空資料,並不會釋放記憶體,一般採用swap函式釋放空間。通過建立臨時拷貝物件,呼叫swap之後來釋放原物件記憶體空間。vector< int>(v).swap(v);注意:這裡的swap是泛型演算法裡的swap函式,不是容器裡的。
- 注意:並不是所有的STL容器的clear成員函式的行為都和vector一樣。事實上,其他容器的clear成員函式都會釋放其記憶體。比如另一個和vector類似的順序容器deque。
deque(雙向佇列):
- 與vector類似,也是基於陣列,但是支援開始端插入元素:push_front;
- 與vector相比元素存取和迭代器動作稍慢,但是記憶體分配方面優於vector
- c++標準建議:vector是那種應該在預設情況下使用的序列。如果大多數插入和刪除操作發生在序列的頭部或尾部時,應該選用deque。
- 使用中控器 map,存的指標,指向實際儲存塊
- 迭代器失效:
a. 在deque容器首部或者尾部插入元素不會使得任何迭代器失效;
b. 在其首部或尾部刪除元素則只會使指向被刪除元素的迭代器失效;
c. 在deque容器的任何其他位置的插入和刪除操作將使指向該容器元素的所有迭代器失效。
關聯式容器
關聯式容器Associative
- containers,元素位置取決於特定的排序準則以及元素值,和插入次序無關。(set、multiset、map、multimap)
- set:一對一, 內部結構採用紅黑樹的平衡二叉樹。自動排序,預設升序,不允許重複值
- multiset:類同set,允許重複值
- map (key,value): 一對多, 內部結構採用紅黑樹的平衡二叉樹。自動排序,預設升序,不允許重複值
- multimap:類同map,允許重複值
紅黑樹:五大特性
- 每個節點或者是黑色,或者是紅色。
- 根節點是黑色。
- 每個葉子節點是黑色。 [注意:這裡葉子節點,是指為空的葉子節點!]
- 如果一個節點是紅色的,則它的子節點必須是黑色的。
從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
紅黑樹插入結點
因為紅黑樹上面的第4個特點,因此當向紅黑樹中插入新的節點時,應該將新節點標註為紅色。向紅黑樹中插入節點看的是插入節點的父節點和叔父節點。
紅黑樹旋轉
右旋
插入D結點,此時的樹不滿足紅黑樹性質,需要旋轉,這裡需要對A進行右旋轉以 A-B軸右旋,對A右旋,A成為B的右孩子,B的右孩子成為A的左孩子
- 同理,若需要左旋,則是以A-B軸右旋,對A左旋,A成為B的左孩子,B的左孩子成為A的右孩子
(二)迭代器-Iterators
迭代器Iterators,用來在一個物件群集(collection of
objects)的元素上進行遍歷。這個物件群集或許是個容器,或許是容器的一部分。迭代器的主要好處是,為所有容器提供了一組很小的公共介面。迭代器以++進行累進,以*進行提領,因而它類似於指標,我們可以把它視為一種smart
pointer- 比如++操作可以遍歷至群集內的下一個元素。至於如何做到,取決於容器內部的資料組織形式。
- 每種容器都提供了自己的迭代器,而這些迭代器能夠了解容器內部的資料結構。
- 迭代器是一種智慧指標,智慧指標定義為儲存指向動態分配物件指標的類,迭代器封裝了指標的同時,還對指標的一些基本操作如*、->、++、==、!=、=進行了過載,使其具有了遍歷複雜資料結構的能力,其遍歷機制取決於所遍歷的資料結構。如operator++運算子,對於陣列就是普通的++下一個元素,對於連結串列則是先去next,再取元素。
迭代器分類:在STL中原生指標也是一種迭代器,除此之外還有五種迭代器
- Input Iterator:此迭代器不允許修改所指的物件,即是只讀的。支援==、!=、++、*、->等操作。
- Output Iterator:允許演算法在這種迭代器所形成的區間上進行只寫操作。支援++、*等操作。
- Forward Iterator:允許演算法在這種迭代器所形成的區間上進行讀寫操作,但只能單向移動,每次只能移動一步。支援Input Iterator和Output Iterator的所有操作。
- Bidirectional Iterator: 允許演算法在這種迭代器所形成的區間上進行讀寫操作,可雙向移動,每次只能移動一步。支援Forward Iterator的所有操作,並另外支援–操作。
Random Access Iterator:包含指標的所有操作,可進行隨機訪問,隨意移動指定的步數。支援前面四種Iterator的所有操作,並另外支援it + n、it - n、it += n、 it -= n、it1 - it2和it[n]等操作。
只有順序容器和關聯容器支援迭代器遍歷,各容器支援的迭代器的類別如下:- vector 隨機訪問
- deque 隨機訪問
- list 雙向
- set 雙向
- multiset 雙向
- map 雙向
- multimap 雙向
- stack 不支援
- queue 不支援
- priority_queue 不支援
三、演算法-Algorithm
演算法Algorithms,用來處理群集內的元素。它們可以出於不同的目的而搜尋、排序、修改、使用那些元素。通過迭代器的協助,我們可以只需編寫一次演算法,就可以將它應用於任意容器,這是因為所有的容器迭代器都提供一致的介面。
四、介面卡-Adapter
- 介面卡是一種類,為已有的類提供新的介面,目的是簡化、約束、使之安全、隱藏或者改變被修改類提供的服務集合
三種類型的介面卡:
- 容器介面卡:用來擴充套件7種基本容器,它們和順序容器相結合構成棧、佇列和優先佇列容器,stack,queue,
priority_queue可以基於vector和deque,採用最大堆來實現,因為需要隨機存取迭代器,只有這兩個; - 迭代器介面卡(反向迭代器、插入迭代器、IO流迭代器)
- 函式介面卡(函式物件介面卡、成員函式介面卡、普通函式介面卡)
- 容器介面卡:用來擴充套件7種基本容器,它們和順序容器相結合構成棧、佇列和優先佇列容器,stack,queue,
五、函式物件(仿函式)-function object
- 一個行為類似函式的物件,它可以沒有引數,也可以帶有若干引數。
- 任何過載了呼叫運算子operator()的類的物件都滿足函式物件的特徵
- 函式物件可以把它稱之為smart function。
- STL中也定義了一些標準的函式物件,如果以功能劃分,可以分為算術運算、關係運算、邏輯運算三大類。為了呼叫這些標準函式物件,需要包含標頭檔案< functional>。
六、分配器-allocator(*)
參考:https://blog.csdn.net/md521/article/details/42046043
負責空間配置與管理。從實現的角度來看,配置器是一個實現了動態空間配置、空間管理、空間釋放的class template。
隱藏在這些容器後的記憶體管理工作是通過STL提供的一個預設的allocator實現的。當然,使用者也可以定製自己的allocator,只要實現allocator模板所定義的介面方法即可,然後通過將自定義的allocator作為模板引數傳遞給STL容器,建立一個使用自定義allocator的STL容器物件,如:
stl::vector< int, UserDefinedAllocator> array;
大多數情況下,STL預設的allocator就已經足夠了。這個allocator是一個由兩級分配器構成的記憶體管理器,當申請的記憶體大小大於128byte時,就啟動第一級分配器通過malloc/free直接向系統的堆空間分配,如果申請的記憶體大小小於128byte時,就啟動第二級分配器,從一個預先分配好的記憶體池中取一塊記憶體交付給使用者,這個記憶體池由16個不同大小(8的倍數,8~128byte)的空閒列表組成,allocator會根據申請記憶體的大小(將這個大小round up成8的倍數)從對應的空閒塊列表取表頭塊給使用者。
這種做法有兩個優點:
(1)小物件的快速分配。小物件是從記憶體池分配的,這個記憶體池是系統呼叫一次malloc分配一塊足夠大的區域給程式備用,當記憶體池耗盡時再向系統申請一塊新的區域,整個過程類似於批發和零售,起先是由allocator向總經商批發一定量的貨物,然後零售給使用者,與每次都總經商要一個貨物再零售給使用者的過程相比,顯然是快捷了。當然,這裡的一個問題時,記憶體池會帶來一些記憶體的浪費,比如當只需分配一個小物件時,為了這個小物件可能要申請一大塊的記憶體池,但這個浪費還是值得的,況且這種情況在實際應用中也並不多見。
(2)避免了記憶體碎片的生成。程式中的小物件的分配極易造成記憶體碎片,給作業系統的記憶體管理帶來了很大壓力,系統中碎片的增多不但會影響記憶體分配的速度,而且會極大地降低記憶體的利用率。以記憶體池組織小物件的記憶體,從系統的角度看,只是一大塊記憶體池,看不到小物件記憶體的分配和釋放。