從大學時就開始學習C++,到現在近5年的時間了卻很少用到STL。現在想想真得是對不起這門語言,也對不起寶貴的五年光陰。我鍾愛C++,所以一定要完全搞懂它,理解它。愛一個人的前提是要懂他(她),愛一門語言也是這樣。鄭重地向C++說聲“對不起!”。我會把不懂你的方面慢慢彌補,做到真正懂你。為了更好地學習STL,我采用邊學習,邊總結,邊寫博客的方法,希望能夠形成一個學習專欄。這樣既可以便於自己隨時翻閱,又可以分享給有需要的人。當然在博客中,我有可能會引用到其它大牛博友的文章。為了尊重原創,我會給出參考博文的鏈接地址。另外,如果大家在文章中發現錯誤,希望在評論下方給出提示建議。
Now,開始學習C++中重要的標准庫STL。
STL簡介
STL的原名是“Standard Template Library”,翻譯過來就是標准模板庫。STL是C++標准庫的一個重要組成部分,主要由六大組件構成。這六大組件是:
容器(Container)、算法(algorithm)、迭代器(iterator)、仿函數(functor)、適配器(adapter)、配置器(allocator)。
1、容器(container)
容器可以分為三類即序列容器、關聯容器和容器適配器。各類具體包含如下所示:
序列容器:vector、list、deque
關聯容器:set、map、multiset、multimap
適配器容器:stack、queue、priority_queue
容器 | 特性 | 所在頭文件 |
向量vector | 在常數時間訪問和修改任意元素,在序列尾部進行插入和刪除時,具有常數時間復雜度。對任意項的插入和刪除的時間復雜度與到末尾的距離成正比,尤其對向量頭的添加和刪除代價比較高。 | <vector> |
雙端隊列deque | 基本上與向量相同,不同點是,雙端隊列的兩端插入和刪除操作也是常量的時間復雜度。 | <deque> |
表list | 對任意元素的訪問與兩端的距離成正比,但對某個位置上插入和刪除時間復雜度為常數。 | <list> |
隊列queue | 插入只可以在尾部進行,刪除、檢索和修改只允許從頭部進行。遵循FIFO的原則。 | <queue> |
棧stack | LIFO:先進后出原則。只允許在序列頭部進行插入、刪除、檢索和修改操作,時間復雜度為常數。 | <stack> |
集合set | 內部實現機制是紅黑樹,每個結點都包含一個元素,結點之間以某種作用於元素對的謂詞排列,沒有兩個不同的元素能夠擁有相同的次序,具有快速查找的功能。 | <set> |
多重集合multiset | 和集合基本相同,但可以支持重復元素。 | <set> |
映射map | 由(鍵,值)對組成的集合,以某種作用於鍵對上的謂詞排序。具有快速查找特性。 | <map> |
多重映射multimap | 支持一鍵對應多個值的特性,具有快速查找功能。 | <map> |
2、算法(Algorithm)
算法部分主要在頭文件<algorithm>,<numeric>,<functional>中。<algoritm>是所有STL頭文件中最大的一個,它是由一大堆模版函數組成的,可以認為每個函數在很大程度上都是獨立的,其中常用到的功能范 圍涉及到比較、交換、查找、遍歷操作、復制、修改、移除、反轉、排序、合並等等。<numeric>體積很小,只包括幾個在序列上面進行簡單數學運算的模板函數,包括加法和乘法在序列上的一些操作。<functional>中則定義了一些模板類,用以聲明函數對象。
3、迭代器(Adapter)
迭代器是用類模板(class template)實現的.重載了* ,-> ,++ ,-- 等運算符。
迭代器分5種:輸入迭代器、輸出迭代器、 前面迭代器、雙向迭代器、 隨機訪問迭代器。
輸入迭代器:向前讀(只允許讀);
輸出迭代器:向前寫(只允許寫);
前向迭代器:向前讀寫;
雙向迭代器:向前后讀寫;
隨機迭代器:隨機讀寫;
4、仿函數(Functor)
仿函數用類模板實現,重載了符號"()"。仿函數,又或叫做函數對象,是STL六大組件之一;仿函數雖然小,但卻極大的拓展了算法的功能,幾乎所有的算法都有仿函數版本。
例如,查找算法find_if就是對find算法的擴展,標准的查找是兩個元素相等就找到了,但是什么是相等在不同情況下卻需要不同的定義,如地址相等,地址和郵編都相等,雖然這些相等的定義在變,但算法本身卻不需要改變,這都多虧了仿函數。仿函數(functor)又稱之為函數對象(function object),其實就是重載了()操作符的struct,沒有什么特別的地方。
如以下代碼定義了一個二元判斷式functor:
struct IntLess { bool operator()(int left, int right) const { return (left < right); } };
仿函數的優勢:
1)仿函數比一般函數靈活。
2)仿函數有類型識別。可以用作模板參數。
3)執行速度上仿函數比函數和指針要更快。
在STL里仿函數最常用的就是作為函數的參數,或者模板的參數。
在STL里有自己預定義的仿函數,比如所有的運算符=,-,*,、比如'<'號的仿函數是less。
// TEMPLATE STRUCT less template<class _Ty = void> struct less : public binary_function<_Ty, _Ty, bool> { // functor for operator< bool operator()(const _Ty& _Left, const _Ty& _Right) const { // apply operator< to operands return (_Left < _Right); } };
less繼承binary_function<_Ty,_Ty,bool>
template<class _Arg1, class _Arg2, class _Result> struct binary_function { // base class for binary functions typedef _Arg1 first_argument_type; typedef _Arg2 second_argument_type; typedef _Result result_type; };
從定義中可以知道binary_function知識做了一些類型的聲明,這樣做就是為了方便安全,提高可復用性。
按照這個規則,我們也可以自定義仿函數:
template <typename type1,typename type2> class func_equal :public binary_function<type1,type2,bool> { inline bool operator()(type1 t1,type2 t2) const//這里的const不能少 { return t1 == t2;//當然這里要overload== } }
之所以const關鍵字修飾函數,是因為const對象只能訪問const修飾的函數。如果一個const對象想使用重載的()函數,編譯過程就會報錯。
小結一下:仿函數就是重載()的class,並且重載函數要有const修飾。自定義仿函數必須要繼承binary_function(二元函數)或者unary_function(一元函數)。其中unary_function的定義如下:
struct unary_function { typedef _A argument_type; typedef _R result_type; };
5、適配器(Adapter)
適配器是用來修改其他組件接口的STL組件,是帶有一個參數的類模板(這個參數是操作的值的數據類型)。STL定義了3種形式的適配器:容器適配器,迭代器適配器,函數適配器。
1)容器適配器:棧(stack)、隊列(queue)、優先(priority_queue)。使用容器適配器,stack就可以被實現為基本容器類型(vector,dequeue,list)的適配。可以把stack看作是某種特殊的vctor,deque或者list容器,只是其操作仍然受到stack本身屬性的限制。queue和priority_queue與之類似。容器適配器的接口更為簡單,只是受限比一般容器要多。
2)迭代器適配器:修改為某些基本容器定義的迭代器的接口的一種STL組件。反向迭代器和插入迭代器都屬於迭代器適配器,迭代器適配器擴展了迭代器的功能。
3)函數適配器:通過轉換或者修改其他函數對象使其功能得到擴展。這一類適配器有否定器(相當於"非"操作)、綁定器、函數指針適配器。函數對象適配器的作用就是使函數轉化為函數對象,或是將多參數的函數對象轉化為少參數的函數對象。
例如:
在STL程序里,有的算法需要一個一元函數作參數,就可以用一個適配器把一個二元函數和一個數值,綁在一起作為一個一元函數傳給算法。
find_if(coll.begin(), coll.end(), bind2nd(greater <int>(), 42));
這句話就是找coll中第一個大於42的元素。
greater <int>(),其實就是">"號,是一個2元函數
bind2nd的兩個參數,要求一個是2元函數,一個是數值,結果是一個1元函數。
bind2nd就是個函數適配器。
6、空間配置器(Allocator)
STL內存配置器為容器分配並管理內存。統一的內存管理使得STL庫的可用性、可移植行、以及效率都有了很大的提升。
SGI-STL的空間配置器有2種,一種僅僅對c語言的malloc和free進行了簡單的封裝,而另一個設計到小塊內存的管理等,運用了內存池技術等。在SGI-STL中默認的空間配置器是第二級的配置器。
SGI使用時std::alloc作為默認的配置器。
- alloc把內存配置和對象構造的操作分開,分別由alloc::allocate()和::construct()負責,同樣內存釋放和對象析夠操作也被分開分別由alloc::deallocate()和::destroy()負責。這樣可以保證高效,因為對於內存分配釋放和構造析夠可以根據具體類型(type traits)進行優化。比如一些類型可以直接使用高效的memset來初始化或者忽略一些析構函數。對於內存分配alloc也提供了2級分配器來應對不同情況的內存分配。
- 第一級配置器直接使用malloc()和free()來分配和釋放內存。第二級視情況采用不同的策略:當需求內存超過128bytes的時候,視為足夠大,便調用第一級配置器;當需求內存小於等於128bytes的時候便采用比較復雜的memeory pool的方式管理內存。
- 無論allocal被定義為第一級配置器還是第二級,SGI還為它包裝一個接口,使得配置的接口能夠符合標准即把配置單位從bytes轉到了元素的大小:
template<class T, class Alloc> class simple_alloc { public: static T* allocate(size_t n) { return 0 == n ? 0 : (T*)Alloc::allocate(n * sizeof(T)); } static T* allocate(void) { return (T*) Alloc::allocate(sizeof(T)); } static void deallocate(T* p, size_t n) { if (0 != n) Alloc::deallocate(p, n * sizeof(T)); } static void deallocate(T* p) { Alloc::deallocate(p, sizeof(T)); } }
內存的基本處理工具,均具有commit或rollback能力。
template<class InputIterator, class ForwardIterator> ForwardIterator uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result); template<class ForwardIterator, class T> void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x); template<class ForwardIterator, class Size, class T> ForwardIterator uninitialized_fill_n(ForwardIterator first, ForwardIterator last, const T& x)
泛型技術
泛型技術的實現方法有:模板、多態等。模板是編譯時決定的,多態是運行時決定的,RTTI也是運行時確定的。
多態是依靠虛表在運行時查表實現的。比如一個類擁有虛方法,那么這個類的實例的內存起始地址就是虛表地址,可以把內存起始地址強制轉換成int*,取得虛表,然后(int*)*(int*)取得虛表里的第一個函數的內存地址,然后強制轉換成函數類型,即可調用來驗證虛表機制。
泛型編程(Generic Programming,以下直接以GP稱呼)是一種全新的程序設計思想,和OO,OB,PO這些為人所熟知的程序設計想法不同的是GP抽象度更高,基於GP設計的組件之間耦合度低,沒有繼承關系,所以其組件間的互交性和擴展性都非常高。我們都知道,任何算法都是作用在一種特定的數據結構上的,最簡單的例子就是快速排序算法最根本的實現條件就是所排序的對象是存貯在數組里面,因為快速排序就是因為要用到數組的隨機存儲特性,即可以在單位時間內交換遠距離的對象,而不只是相臨的兩個對象,而如果用鏈表去存儲對象,由於在鏈表中取得對象的時間是線性的即O[n],這樣將使快速排序失去其快速的特點。也就是說,我們在設計一種算法的時候,我們總是先要考慮其應用的數據結構,比如數組查找,聯表查找,樹查找,圖查找其核心都是查找,但因為作用的數據結構不同將有多種不同的表現形式。數據結構和算法之間這樣密切的關系一直是我們以前的認識。泛型設計的根本思想就是想把算法和其作用的數據結構分離,也就是說,我們設計算法的時候並不去考慮我們設計的算法將作用於何種數據結構之上。泛型設計的理想狀態是一個查找算法將可以作用於數組,聯表,樹,圖等各種數據結構之上,變成一個通用的,泛型的算法。
六大組件的關系結構
Container 通過Allocator獲得數據存儲空間;Algorithm 通過Iterator存取Container中的內容;Functor可以協助Algoritm完成不同策略;Adapter可以修飾Container、Algorithm、Iterator。
參考鏈接: