【轉載】STL"源碼"剖析-重點知識總結

來源:互聯網
上載者:User

標籤:

原文:STL"源碼"剖析-重點知識總結

 

STL是C++重要的組件之一,大學時看過《STL源碼剖析》這本書,這幾天複習了一下,總結出以下LZ認為比較重要的知識點,內容有點略多 :)

1、STL概述

STL提供六大組件,彼此可以組合套用:

  • 容器(Containers):各種資料結構,如:vector、list、deque、set、map。用來存放資料。從實現的角度來看,STL容器是一種class template。
  • 演算法(algorithms):各種常用演算法,如:sort、search、copy、erase。從實現的角度來看,STL演算法是一種 function template。
  • 迭代器(iterators):容器與演算法之間的膠合劑,是所謂的“泛型指標”。共有五種類型,以及其他衍生變化。從實現的角度來看,迭代器是一種將 operator*、operator->、operator++、operator- - 等指標相關操作進行重載的class template。所有STL容器都有自己專屬的迭代器,只有容器本身才知道如何遍曆自己的元素。原生指標(native pointer)也是一種迭代器。
  • 仿函數(functors):行為類似函數,可作為演算法的某種策略(policy)。從實現的角度來看,仿函數是一種重載了operator()的class或class template。一般的函數指標也可視為狹義的仿函數。
  • 配接器(adapters):一種用來修飾容器、仿函數、迭代器介面的東西。例如:STL提供的queue 和 stack,雖然看似容器,但其實只能算是一種容器配接器,因為它們的底部完全藉助deque,所有操作都由底層的deque供應。改變 functors介面者,稱為function adapter;改變 container 介面者,稱為container adapter;改變iterator介面者,稱為iterator adapter。
  • 配置器(allocators):負責空間配置與管理。從實現的角度來看,配置器是一個實現了動態空間配置、空間管理、空間釋放的class template。

 

STL六大組件的互動關係

 

一些可能令人困惑的C++文法糖:

  1. 靜態常量整數成員(double就不行)在class內部直接初始化
  2. 靜態成員只能在類外初始化,且初始化時不加static
  3. 基類夠建構函式中調用virtual函數實際調用的是基類中的virtual函數(這點和Java不同)
  4. 任何一個STL演算法,都需要獲得有一對迭代器(泛型指標)所指示的區間用以表示操作的範圍。這一對迭代器表示的就是前閉後開區間

 

泛型指標、原生指標和智能指標
  • 泛型指標有多種含義。指void*指標,可以指向任意資料類型,因此具有“泛型”含義。指具有指標特性的泛型資料結構,包含泛型的迭代器、智能指標等。廣義的迭代器是一種不透明指標,能夠實現遍曆訪問操作。通常所說的迭代器是指狹義的迭代器,即基於C++的STL中基於泛型的iterator_traits實現的類的執行個體。總體來說,泛型指標和迭代器是兩個不同的概念,其中的交集則是通常提到的迭代器類。
  • 原生指標就是普通指標,與它相對的是使用起來行為上像指標,但卻不是指標。說“原生”是指“最簡樸最基本的那一種”。因為現在很多東西都抽象化理論化了,所以“以前的那種最簡樸最基本的指標”只是一個抽象概念(比如iterator)的表現形式之一。
  • 智能指標是C++裡面的概念:由於 C++ 語言沒有自動記憶體回收機制,程式員每次得自己處理記憶體相關問題,但用智能指標便可以有效緩解這類問題。引入智能指標可以防止出現懸垂指標的情況,一般是把指標封裝到一個稱之為智能指標類中,這個類中另外還封裝了一個使用計數器,對指標的複製等操作將導致該計數器的值加1,對指標的delete操作則會減1,值為0時,指標為NULL

 

2、迭代器

  STL的中心思想是:將資料容器和演算法分隔開,彼此獨立設計,最後再用黏合劑將它們撮合在一起。容器和演算法的泛型化,可以用C++的class template和function template來實現,而二者的黏合劑就是迭代器了。

迭代器是一種智能指標

  與其說迭代器是一種指標,不如說迭代器是一種智能指標,它將指標進行了一層封裝,既包含了原生指標的靈活和強大,也加上很多重要的特性,使其能發揮更大的作用以及能更好的使用。迭代器對指標的一些基本操作如*、->、++、==、!=、=進行了重載,使其具有了遍曆複雜資料結構的能力,其遍曆機製取決於所遍曆的資料結構。下面上一段代碼,瞭解一下迭代器的“智能”:

template<typename T>  class Iterator  {  public:      Iterator& operator++();      //...  private:       T *m_ptr;  };  

  對於不同的資料容器,以上Iterator類中的成員函數operator++的實現會各不相同,例如,對於數組的可能實現如下:

//對於數組的實現  template<typename T>  Iterator& operator++()  {      ++m_ptr;      retrun *this;  }

  對於鏈表,它會有一個類似於next的成員函數用於擷取下一個結點,其可能實現如下:

//對於鏈表的實現  template<typename T>  Iterator& operator++()  {     m_ptr = m_ptr->next();//next()用於擷取鏈表的下一個節點      return *this;  }  

  iterator首先要對iterator指向對象的實現細節有非常豐富的瞭解,所以iterator為了不暴露所指向對象的資訊,乾脆就將iterator的實現由各個容器的設計者來實現好了。STL將迭代器的實現交給了容器,每種容器都會以嵌套的方式在內部定義專屬的迭代器。各種迭代器的介面相同,內部實現卻不相同,這也直接體現了泛型程式設計的概念。

迭代器使用樣本
#include <iostream>  #include <vector>  #include <list>  #include <algorithm>  using namespace std;int main(int argc, const char *argv[]){    int arr[5] = { 1, 2, 3, 4, 5 };    vector<int> iVec(arr, arr + 5);//定義容器vector      list <int> iList(arr, arr + 5);//定義容器list      //在容器iVec的頭部和尾部之間尋找整形數3      vector<int>::iterator iter1 = find(iVec.begin(), iVec.end(), 3);    if (iter1 == iVec.end())        cout << "3 not found" << endl;    else        cout << "3 found" << endl;    //在容器iList的頭部和尾部之間尋找整形數4      list<int>::iterator iter2 = find(iList.begin(), iList.end(), 4);    if (iter2 == iList.end())        cout << "4 not found" << endl;    else        cout << "4 found" << endl;    system("pause");    return 0;}

  從上面迭代器的使用中可以看到,迭代器依附於具體的容器,即不同的容器有不同的迭代器實現,同時,我們也看到,對於演算法find來說,只要給它傳入不同的迭代器,即可對不同的容器進行尋找操作。通過迭代器的穿針引線,有效地實現了演算法對不同容器的訪問,這也是迭代器的設計目的。

 

3、序列式容器

  所謂序列式容器,其中的元素都可序,但未必有序,C++本身內建了一個序列式容器array,STL另外提供了vector、list、deque、stack、queue、priority-queue等序列式容器。其中stack和queue由於只是deque改頭換面而來,技術上被歸為一種配接器 (adapter)。

vector

  vector採用的資料結構非常簡單:線性連續空間。它以兩個迭代器start和finish分別指向配置得來的連續空間中目前已被使用的範圍,並以迭代器end_of_storage指向整塊連續空間(含備用空間)的尾端。

template <class T, class Alloc = alloc>class vector {    ...protected:    iterator start;    //表示    iterator finish;    iterator end_of_storage;    ...};

 

  注意:所謂動態增加大小,並不是在原來空間之後接續新空間(因為無法保證原空間之後尚有可供分配的空間),而是以原來大小的的兩倍另外分配一塊較大空間,然後將原內容拷貝過來,然後才開始在原內容之後構造新元素,並釋放原空間。因此,對vector的任何操作,一旦引起空間重新設定,指向原vector的所有迭代器就都失效啦。

 

vector變數的大小分析

  vector類中有3個迭代器域(也就是指標域),所以大小至少為12位元組。

測試環境:win7 64位 VS2013

測試代碼:

#include <iostream>#include <vector>using namespace std;int main(void){    vector<int> a(5, 0); //16    cout << sizeof(a) << endl;    cout << (int)(void *)&a << endl;    cout << (int)(void *)&a[0] << endl;    cout << (int)(void *)&a[a.size() - 1] << endl;    cout << endl;    cout << *((int *)&a) << endl;    cout << *(((int *)&a) + 1) << endl;    cout << *(((int *)&a) + 2) << endl;    cout << *(((int *)&a) + 3) << endl;    cout << endl;    cout << a.size() << endl;    cout << a.capacity() << endl;    system("pause");    return 0;}

  測試結果顯示此時vector的大小為16位元組,分別包括start、finish、end_of_storage成員,剩下的4個位元組暫時不知道代表什麼意思… :(

測試環境:Ubuntu12.04 codeblocks10.05

  測試代碼:

#include <iostream>#include <vector>using namespace std;int main(void){    vector<int> a(5, 0); //12    cout << sizeof(a) << endl;    cout << (int)(void *)&a << endl;    cout << (int)(void *)&a[0] << endl;    cout << (int)(void *)&a[a.size() - 1] << endl;    cout << endl;    cout << *((int *)&a) << endl;    cout << *(((int *)&a) + 1) << endl;    cout << *(((int *)&a) + 2) << endl;    cout << *(((int *)&a) + 3) << endl;    cout << endl;    cout << a.size() << endl;    cout << a.capacity() << endl;    return 0;}

  測試結果顯示此時vector的大小為12位元組,包括start、finish、end_of_storage成員

小結

  win和Ubuntu所用的STL的版本是不一樣的,不同的STL所使用的vector類也不同,有著不同的容器管理方式。

list

  相對於vector的連續線性空間,list就顯得複雜許多,它的好處就是插入或刪除一個元素,就配置或刪除一個元素空間。對於任何位置的元素的插入或刪除,list永遠是常數時間。

  list本身和節點是不同的結構,需要分開設計。以下是STL list的節點node結構:

template <class T>class __list_node {    typedef void* void_pointer;    void_pointer prev;    void_pointer next;    T data;};

這是一個雙向鏈表

list資料結構

       SGI list不僅是一個雙向鏈表,而且是一個環狀雙向鏈表。只需一個指標就可遍曆整個鏈表。

deque

  deque和vector的最大差異,一在於deque允許常數時間內對起頭端進行插入或移除操作,二在於deque沒有所謂容量(capacity)概念,因為它是以分段連續空間組合而成,隨時可以增加一段新的空間串連起來。

  deque由一段一段連續空間組成,一旦有必要在deque的前端或尾端增加新空間,便配置一段連續空間,串接在整個deque的前端或尾端。deque的最大任務,便是在這些分段的連續空間上,維護其整體連續的假象,並提供隨機存取的介面,避開了“重新設定、複製、釋放”的輪迴,代價是複雜的迭代器結構。

deque迭代器

  迭代器首先必須指出分段連續空間在哪裡,其次它必須能夠判斷自己是否已經處在緩衝區的邊緣,如果是,一旦前進或後退就必須跳躍下一個緩衝區,為了能夠正常跳躍,deque必須隨時掌握管控中心。

迭代器結構:

template <class T, class Ref, class Ptr, size_t BufSiz>struct __deque_iterator { // 未繼承 std::iterator    // 保持迭代器的串連    T* cur; // 此迭代器所指之緩衝區的現行( current)元素    T* first; // 此迭代器所指之緩衝區的的頭    T* last; // 此迭代器所指之緩衝區的的尾(含備用空間)    map_pointer node; // 指向管控中心    ...};

  假如deque中已經包含了20個元素了,緩衝區大小為8,則記憶體布局如下:

注意:deque最初狀態(無任何元素)保有一個緩衝區,因此,clear()完成之後回到初始狀態,也一樣會保留一個緩衝區。

stack

  tack是一種先進後出(First In Last Out,FILO)的資料結構,它只有一個出口。stack允許增加元素、移除元素、取得最頂端元素。但除了最頂端外,沒有任何其他方法可以存取,stack的其他元素,換言之,stack不允許有遍曆行為。stack預設以deque為底層容器。

queue

  queue是一種先進先出(First In First Out,FIFO)的資料結構,它有兩個出口,允許增加元素、移除元素、從最底端加入元素、取得最頂端元素。但除了最底端可以加入、最頂端可以取出外,沒有任何其他方法可以存取queue的其他元素,換言之,queue不允許有遍曆行為。queue預設以deque為底層容器。

heap

  heap並不歸屬於STL容器組件,它是個幕後英雄,扮演prority queue的助手。priority queue允許使用者以任何次序將任何元素推入容器內,但取出時一定是按照優先順序最高的元素開始取。binary max heap正好具有這樣的特性,適合作為priority queue的底層機制。heap預設建立的是大堆。

heap測試案例:

#include <iostream>#include <queue>#include <algorithm>using namespace std;template <class T>struct display{    void operator()(const T &x)    {        cout << x << " ";    }};/// heap預設為大堆,以下設定為建立小堆template <typename T>struct greator{    bool operator()(const T &x, const T &y)    {        return x > y;    }};int main(void){    int ia[9] = { 0, 1, 2, 3, 4, 8, 9, 3, 5 };    vector<int> ivec(ia, ia + 9);    make_heap(ivec.begin(), ivec.end(), greator<int>()); //注意:此函數調用時,新元素應已止於底部容器的尾端    for_each(ivec.begin(), ivec.end(), display<int>());    cout << endl;    ivec.push_back(7);    push_heap(ivec.begin(), ivec.end(), greator<int>());    for_each(ivec.begin(), ivec.end(), display<int>());    cout << endl;    pop_heap(ivec.begin(), ivec.end(), greator<int>());    cout << ivec.back() << endl;    ivec.pop_back();    for_each(ivec.begin(), ivec.end(), display<int>());    cout << endl;    sort_heap(ivec.begin(), ivec.end(), greator<int>());    for_each(ivec.begin(), ivec.end(), display<int>());    cout << endl;    system("pause");    return 0;}
priority_queue

  priority_queue是一個擁有權值的queue,它允許加入新元素、移除舊元素、審視元素值等功能。由於是一個queue,所以只允許在底端加入元素,從頂端取出元素,除此之外別無其他存取元素方法。priority_queue內的元素並非按照被推入的順序排列,而是自動按照元素的權值排列。權值最高者排在前面。

  預設情況下priority_queue利用max-heap按成,後者是一個以vector為底層容器的complate binary tree。

priority_queue測試案例:

#include <iostream>#include <queue>#include <algorithm>using namespace std;int main(void){    int ia[9] = { 0, 1, 2, 3, 4, 8, 9, 3, 5 };    vector<int> ivec(ia, ia + 9);    priority_queue<int> ipq(ivec.begin(), ivec.end());    ipq.push(7);    ipq.push(23);    while (!ipq.empty())    {        cout << ipq.top() << " ";        ipq.pop();    }    cout << endl;    system("pause");    return 0;}

 

4、關聯性容器

  set和map底層資料結構都是紅/黑樹狀結構,紅/黑樹狀結構的data域段為pair<key, value>類型。關於紅/黑樹狀結構更多知識請點擊:深入理解紅/黑樹狀結構。

set

  set的所有元素都會根據元素的索引值自動排序。set的元素不像map那樣可以同時擁有實值(value)和索引值(key),set元素的索引值就是實值,實值就是索引值,set不允許有兩個相同的元素。Set元素不能改變,在set源碼中,set<T>::iterator被定義為底層TB-tree的const_iterator,杜絕寫入操作,也就是說,set iterator是一種constant iterators(相對於mutable iterators)

測試案例(讓set從大到小存放元素):

#include <iostream>#include <set>#include <functional>using namespace std;/// set預設是從小到大排列,以下是讓set從大到小排列template <typename T>struct greator{    bool operator()(const T &x, const T &y)    {        return x > y;    }};int main(void){    set<int, greator<int>> iset;    iset.insert(12);    iset.insert(1);    iset.insert(24);    for (set<int>::const_iterator iter = iset.begin(); iter != iset.end(); iter++)    {        cout << *iter << " ";    }    cout << endl;    system("pause");    return 0;}
map

   map的所有元素都會根據元素的索引值自動排序。map的所有元素都是pair,同時擁有實值(value)和索引值(key)。pair的第一元素為索引值,第二元素為實值。map不允許有兩個相同的索引值。

  如果通過map的迭代器改變元素的索引值,這樣是不行的,因為map元素的索引值關係到map元素的排列規則。任意改變map元素索引值都會破壞map組織。如果修改元素的實值,這是可以的,因為map元素的實值不影響map元素的排列規則。因此,map iterator既不是一種constant iterators,也不是一種mutable iterators。

測試案例(map從大到小存放元素):

#include <iostream>#include <string>#include <map>#include <functional>using namespace std;/// map預設是從小到大排列,以下是讓map從大到小排列template <typename T>struct greator{    bool operator()(const T x, const T y)    {        return x > y;    }};int main(void){    map<int, string, greator<int>> imap;    imap[3] = "333";    imap[1] = "333";    imap[2] = "333";    for (map<int, string>::const_iterator iter = imap.begin(); iter != imap.end(); iter++)    {        cout << iter->first << ": " << iter->second << endl;    }    system("pause");    return 0;}
multiset/multimap

  multiset的特性以及用法和set完全相同,唯一的差別在於它允許索引值重複,因此它的插入操作採用的是底層機制RB-tree的insert_equal()而非insert_unique()。

  multimap的特性以及用法和map完全相同,唯一的差別在於它允許索引值重複,因此它的插入操作採用的是底層機制RB-tree的insert_equal()而非insert_unique()。

hashtable (底層資料結構)

  二叉搜尋樹具有對數平均時間表現,但這樣的表現構造在一個假設上:輸入資料有足夠的隨機性。hashtable這種結構在插入、刪除、尋找具有“常數平均時間”,而且這種表現是以統計為基礎,不需依賴元素的隨機性。

  hashtable底層資料結構為分離串連法的hash表,如下所示:

  hashtable中的buckets使用的是vector資料結構,當插入一個元素時,找到該插入哪個buckets的插槽,然後遍曆該插槽指向的鏈表,如果有相同的元素,就返回;否則的話就將該元素插入到該鏈表的頭部。(當然,如果是multi版本的話,是可以插入重複元素的,此時插入過程為:當插入一個元素時,找到該插入哪個buckets的插槽,然後遍曆該插槽指向的鏈表,如果有相同的元素,就將新節點插入到該相同元素的後面;如果沒有相同的元素,產生新節點,插入到鏈表頭部)

  當調用成員函數clear()後,buckets vector並未釋放空間,仍保留原來大小,只是刪除了buckets所串連的鏈表。

hash_multimap插入式的圖示說明

hash_set

  運用set,為的是快速搜尋元素。這一點,不論其底層是RB-tree或是hashtable,都可以完成任務,但是,RB-tree有自動排序功能而hashtable沒有,即set的元素有自動排序功能而hash_set沒有。

測試代碼:

 hash_set測試代碼hash_map

  hash_map以hashtable為底層結構,由於hash_map所提供的操作介面,hashtable都提供了,所以幾乎所有的hash_map操作行為都是轉調用hashtable的操作行為結果。RB-tree有自動排序功能而hashtable沒有,反映出來的結果就是,map的元素有自動排序功能而hash_map沒有。

測試代碼:  

 hash_map測試代碼hash_multiset/hash_multimap

  hash_multiset的特性與multiset完全相同,唯一的差別在於它的底層機制是hashtable,因此,hash_multiset的元素是不會自動排序的。

  hash_multimap的特性與multimap完全相同,唯一的差別在於它的底層機制是hashtable,因此,hash_multimap的元素是不會自動排序的。

hash_multimap測試案例:

#include <iostream>#include <hash_map>#include <cstring>#include <algorithm>#include <string>using namespace std;template <typename T>struct print{    void operator()(const T &x)    {        cout << x.first << ": " << x.second << endl;    }};int main(void){    hash_multimap<int, string> hmap;    hmap.insert(pair<int, string>(2, "32"));    hmap.insert(pair<int, string>(2, "22"));    hmap.insert(pair<int, string>(2, "12"));    hmap.insert(pair<int, string>(2, "2"));    for_each(hmap.begin(), hmap.end(), print<pair<int, string>>());    return 0;}

在vs2013(windows 7 64位)下運行結果為:

在Kali2.0中運行(程式需添加using namespace __gun_cxx)結果為:

  由運行結果可知,不同的系統所用的STL是有差別的,不同的STL的hash_table衝突解決方案不一樣。

 

參考:

  1、《STL源碼剖析》

  2、http://blog.csdn.net/shudou/article/details/11099931

  3、http://www.cplusplus.com/search.do?q=slist

 分類: C/C++

【轉載】STL"源碼"剖析-重點知識總結

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.