1. 程式人生 > >空間配置器,迭代器,容器,介面卡

空間配置器,迭代器,容器,介面卡

一、空間配置器

下面先總體介紹一下空間配置器。空間配置器的作用是在底層為上層的各種容器提供儲存空間,需要多少分配多少,一般分配的比你需要的更多。打個不恰當的比喻,空間配置器和容器之間的關係,相當於將軍和糧草的關係。當然了,容器相當於將軍,它在陣前殺敵,衝鋒陷陣,處理各種事情;而空間配置器就相當於糧草,給前陣提供源源不斷的供給;如果一個將軍想打勝仗,那必須後方糧草充足才行。

空間配置器有兩種,一種是一級空間配置器,另一種是二級空間配置器。這兩種空間配置器的分水嶺在於使用者申請的記憶體空間大小,如果申請的位元組數大於128bytes,那麼使用一級空間配置器;反之則使用二級空間配置器。下面分別來介紹一下這兩種空間配置器。注:空間配置器的原始碼在 <Stl_alloc.h> 這個標頭檔案中.

1.一級空間配置器

一級空間配置器__malloc_alloc_template,使用情況是在需要分配的位元組數超過128bytes時,它有兩個關鍵的函式,一個是allocate()函式,另一個是deacllocate()函式。

(1)allocate()函式

這個函式的功能是申請記憶體空間,由於需要的記憶體空間較大(大於128bytes),所以在這個函式裡面呼叫的malloc()函式直接分配記憶體空間。這樣會存在一個問題,就是如果記憶體空間不夠的話怎麼辦?我們能想到的問題,STL的設計者早就想到了,這個時候對呼叫_S_oom_malloc()這個函式,這個函式中會有一個死迴圈,這個死迴圈的作用就是不斷的進行malloc,只有malloc我需要的記憶體空間了我才會結束這個迴圈。

(2)deallocate()函式

這個函式的功能是釋放記憶體空間,既然在allocate()內使用malloc()申請記憶體,那再這裡當仁不讓的應該用free()來釋放記憶體空間,malloc()和free()就是我們生活中的好基友,成雙成對出現,不然會發生記憶體洩露。

(3)reallocate()
我們申請的記憶體的時候當然也可以使用reallocate()這個函式,原理和上面類似,底層則呼叫的是realloc()這個函式。

2.二級空間配置器

二級空間配置器__default_alloc_template,之所以會出現二級空間配置器,原因在於要解決小塊記憶體頻繁申請和釋放所帶來的開銷問題,以及小塊記憶體分配造成的記憶體碎片問題。
對於記憶體頻繁分配和回收帶來的問題解決的方法是使用記憶體池(memory pool)。預先分配好一大塊記憶體,當需要的時候直接進行分配,這樣比臨時分配產生的開銷小很多。而對於小塊記憶體分配造成的記憶體碎片問題,解決方式是將預先分配的一大塊記憶體按一定的方式組織起來,便於應付各種大小的小塊記憶體分配,這裡是使用了一個有16個元素的free-list陣列。

(1)空間配置函式allocate()

在二級空間配置器的空間配置函式中,會首先判斷當前申請的位元組數是否超過128bytes,如果超過了則呼叫一級空間配置器的空間配置函式。如果沒有超過,則根據要申請的位元組數按8位元組對齊為它分配記憶體,比如說,如果需要申請23位元組,則按8位元組對齊的話就會分配24位元組(下面圖中的2號坑);如果需要56位元組,則正好分配一個位元組數為56的區塊(下圖中的6號),依此類推。如果記憶體不夠,就會呼叫refill()函式,在這個函式裡面,它會想盡一切辦法,不擇手段的獲取一起能利用的記憶體空間。
在這裡插入圖片描述

(2)空間釋放函式deallocate()

該函式首先判斷區塊的大小,大於128bytes就呼叫一級空間配置器,小於128bytes就找出對應的free list,將區塊回收。

二、迭代器和traits程式設計技法

1.迭代器
iteratro模式的定義是:提供一種方法,使之能夠依序尋訪某個聚合物(容器)所含的各個元素,而又無需要改聚合物的內部表述方式。
迭代器是一種類似指標的物件,而指標的各種行為中最常見也最重要的便是解引用和成員訪問。

2.Traits程式設計技法

簡單來說,就是使用函式模板和類模板推匯出型別引數的具體型別,然後通過具體的資料型別,來定義過載函式,實現在不同情況下呼叫合適的過載函式。
即通過函式模板來進行型別萃取,具體萃取方法這裡就不細說,有興趣的可以看《STL原始碼剖析》第三章。

三、序列式容器

    這是我們介紹的重點,這部分的原始碼不難看懂,所以在這裡我只是進行一個簡單的總結,以及對一些相關問題說出自己的看法和理解。

1.vector

vector本質上相當於一個動態陣列,對於陣列大家能想到的第一個特點無非就是隨機訪問,而對於插入刪除操作對於陣列來說是軟肋。所以陣列不適用於有頻繁插入刪除操作的情況。

vector維護的是一個連續線性空間,當記憶體不夠時,只有一個辦法就是重新尋找更大的空間(一般情況都是新配置的記憶體空間都是原記憶體空間的兩倍),然後將原空間的元素拷貝過去,最後釋放原記憶體空間。 這裡提醒特別注意的一點,對vector的任何操作,一旦引起空間重新配置,指向原vector的所有迭代器就都失效了,這是程式設計師易犯的一個錯誤,務必小心。

2.list

list本質是一個雙向連結串列,而對於連結串列大家也肯定很熟悉,最大的缺點是,當需要訪問一個元素時需要遍歷整個連結串列來進行查詢;而它的優點則是對於插入和刪除操作很方便,可以在O(1)時間內完成。
list的記憶體管理也比較簡單,就是插入元素時就分配一個節點,然後將該節點插入list中;當刪除一個元素就將該節點的記憶體釋放掉。有種即插即用的感覺。

3.deque

deque本質是雙端佇列。它結合了vector和list這兩者。它的特點是可以隨機訪問,以及可以在兩端快速插入和刪除。和vector相比,可以看成是一個vector陣列。它的組織結構如下圖,每個map元素都指向一段連續空間,那段連續空間也是我們真正儲存資料的地方,因此map也稱為中控器:
在這裡插入圖片描述
deque是由一段一段的定量連續空間構成。一旦有必要在deque的前端或尾端增加新空間,便配置一段定量的連續空間,串接在整個deque的頭端或尾端。deque的最大任務就是在這些分段的定量連續空間上,維護其整體連續的假象,並提供隨機存取的介面。
下面是中控器map、迭代器和緩衝區之間的關係:
在這裡插入圖片描述
接下來說說幾種比較特殊的情況,在當前緩衝區滿了以後如果繼續插入元素,這時會重新分配一個緩衝區,並讓map中的一個元素指向這片緩衝區,將新插入的元素存放在新開闢的緩衝區中。另外一種情況是,當刪除緩衝區中的一個元素以後,緩衝區為空,這時會將這個緩衝區釋放掉,以及釋放指向這個緩衝區map元素。第三種情況是,如果再插入一個元素以後,發現map所指的緩衝區全部滿了,這時候不僅要開闢新的緩衝區,還要擴充套件map的空間。

四、介面卡

介面卡本身沒有資料結構,只是底層利用容器的操作來是實現特定的需求。這裡介紹stack、queue和priority_queue這三種介面卡。

1.stack

stack本質是一個操作受限在一端的線性表。特點就是每次操作只能在棧頂進行,底層使用容器deque來實現自己的操作。 棧沒有迭代器,這個也好理解,因為迭代器對棧來說沒有什麼意義,因為棧的操作有特定限制,如果有迭代器則無疑會破壞這種吸限制。

2.queue

queue本質是一個操作受限在兩端的線性表,而且只能在隊首刪除元素,只能在隊尾插入元素。特點是FIFO,底層也是使用deque來實現自己的操作。 同理,佇列也沒有迭代器。

3.priority_queue

優先順序佇列,顧名思義,priority_queue是一個擁有權值觀念的queue,它允許加入新元素、移除舊元素、審視元素值等功能。其內的元素按照元素的權值排雷,權值最高者,排在最前面。 預設情況下,priority_queue利用max_heap完成,後者是一個以vector表現的完全二叉樹。max-heap可以滿足priority_queue所需要的“權值高低自動遞減排序”的特性。

五.總結

1.各種容器使用的情況

容器名 :本質 ( 適用情況)
(1)vector :動態陣列( 隨機訪問、不適用於頻繁插入刪除的情況)
(2)list : 雙向連結串列(迴圈) ( 適用於頻繁插入刪除,不適用於需要隨機訪問的情況)
(3)deque :雙端佇列 (適用於隨機訪問,以及在兩端進行插入刪除操作的情況)

2.介面卡
介面卡名 (底層實現)
stack (deque)
queue (deque)
priority_queue (max-heap+vector)
3.為什麼STL中stack底層使用deque,而不使用vector和list?
棧只需進行入棧和出棧操作,而且這兩個操作基本上在一個程式中都會有,基本不存在說一個程式只入棧而不出棧這種,對於這種情況下,如果使用vector作為底層來實現stack存在的一個很大問題。舉一個例子來說的話,就是在程式中剛開始大量入棧,導致vector會開闢大量連續空間(當然這個開闢的過程還涉及重新分配),之後如果進行出棧到棧空,這時分配給vector的空間不會減小,而會一直保持之前佔有的大塊連續空間,導致很大的浪費。而對於deque來說就不存在這樣的問題。

原文:https://blog.csdn.net/kang___xi/article/details/79564088