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

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

一、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六大組件的交互關係node

 

一些可能使人困惑的C++語法糖:ios

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

 

泛型指針、原生指針和智能指針

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

 

二、迭代器

  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的成員函數用於獲取下一個結點,其可能實現以下:windows

//對於鏈表的實現  
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來講,只要給它傳入不一樣的迭代器,便可對不一樣的容器進行查找操做。經過迭代器的穿針引線,有效地實現了算法對不一樣容器的訪問,這也是迭代器的設計目的。數據結構

 

三、序列式容器

  所謂序列式容器,其中的元素均可序,但未必有序,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;
}

 

四、關聯性容器

  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沒有。

測試代碼:

#include <iostream>
#include <hash_set>
#include <cstring>
using namespace std;
using namespace __gnu_cxx; // gcc編譯器要加上這一句,不然編譯出錯

struct eqstr
{
    bool operator()(const char *s1, const char *s2)
    {
        return strcmp(s1, s2) == 0;
    }
};

void lookup(const hash_set<const char *> &Set, const char *word)
{
    hash_set<const char *>::const_iterator iter
        = Set.find(word);

    cout << word << ": " << (iter != Set.end() ? "present" : "not present") << endl;
}

int main(void)
{
    hash_set<const char *> Set;

    Set.insert("kiwi");
    Set.insert("plum");
    Set.insert("apple");
    Set.insert("mango");
    Set.insert("apricot");
    Set.insert("banana");

    lookup(Set, "mango");
    lookup(Set, "apple");
    lookup(Set, "durian");

    hash_set<const char *>::const_iterator iter;
    for (iter = Set.begin(); iter != Set.end(); iter++)
        cout << *iter << " ";
    cout << endl;

    return 0;
}
hash_set測試代碼

hash_map

  hash_map以hashtable爲底層結構,因爲hash_map所提供的操做接口,hashtable都提供了,因此幾乎全部的hash_map操做行爲都是轉調用hashtable的操做行爲結果。RB-tree有自動排序功能而hashtable沒有,反映出來的結果就是,map的元素有自動排序功能而hash_map沒有。

測試代碼:  

#include <iostream>
#include <hash_map>
#include <cstring>
#include <algorithm>
using namespace std;

template <typename T>
struct print
{
    void operator()(const T &x)
    {
        cout << x.first << ": " << x.second << endl;
    }
};

int main(void)
{
    hash_map<char *, int> days;

    days["january"] = 31;
    days["february"] = 28;
    days["march"] = 31;
    days["april"] = 30;
    days["may"] = 31;
    days["june"] = 30;

    cout << "march: " << days["march"] << endl;
    cout << "june: " << days["june"] << endl;

    cout << "the total elements of hash_map:" << endl;
    for_each(days.begin(), days.end(), print<pair<char *, int>>());

    system("pause");
    return 0;
}
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衝突解決方法不同。

 

參考:

  一、《STL源碼剖析》

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

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

相關文章
相關標籤/搜索