C++ STL程式設計
一、STL簡介
1.1 初識STL
STL(Standard Template Library,即標準模版庫)是一個具有工業級強度的,高效的C++程式庫。它被容納於C++標準程式庫(C++ Standard Library)中,是ANSI/ISO C++標準中最新的也是極具革命性的一部分。該庫包含了諸多在電腦科學領域所常用的基本資料結構和基本演算法。為廣大C++程式設計師提供了一個可擴充套件的應用框架,高度體現了軟體的可複用性。這種現象有些類似於Microsoft Visual C++中的MFC(Microsoft Foundation Class Library),或者是Borland C++ Builder中的VCL(Virsual Component Library)。
從邏輯層次來看,在STL中體現了泛型化程式設計思想(generic programming),引入諸多新的名詞,比如像需求(requirements),概念(concept),模型(model),容器(container),演算法(algorithmn),迭代子(iterator)等。與OOP(object-oriented programming)中多型(polymorphism)一樣,泛型也是一種軟體的複用技術。
從實現層次看,整個STL是以一種型別引數化(type paramterized)的方式實現的,這種方式基於一個在早先C++標準中沒有出現的語言特性——模板(template)。如果查閱任何一個版本的STL原始碼,你就會發現,模板作為構成整個STL的基石是一件千真萬確的事。除此之外,還有許多C++的新特性為STL的實現提供了方便。
1.2 STL的歷史
被譽為STL之父的Alexander Stepanov,出生於蘇聯莫斯科,早在20世紀70年代後半期,他便已經開始考慮,在保證效率的前提下,將演算法從諸多具體應用之中抽象出來的可能性,這便是後來泛型化思想的雛形。為了驗證自己的思想,他和紐約州立大學教授Deepak Kapur,倫塞里爾技術學院教授David Musser共同開發了一種叫做Tecton的語言。儘管這次嘗試最終沒有取得實用性的成果,但卻給了Stepanov很大的啟示。
在隨後的幾年中,他又和David Musser等人先後用Schema語言(一種Lisp語言的變種)和Ada語言建立了一些大型程式庫。這其間,Alexander Stepanov開始意識到,在當時的面向物件程式設計思想中所存在的一些問題,比如抽象資料型別概念所存在的缺陷。Stepanov希望通過對軟體領域中各組成部分的分類,逐漸形成一種軟體設計的概念性框架。
1987年左右,在貝爾實驗室工作的Alexander Stepanov開始首次採用C++語言進行泛型軟體庫的研究。但遺憾的是,當時的C++語言還沒有引入模板(template)的語法,現在我們可以清楚的看到,模板概念之於STL實現,是何等重要。是時使然,採用繼承機制是別無選擇的。儘管如此,Stepanov還是開發出了一個龐大的演算法庫。與此同時,在與Andrew Koenig(前ISO C++標準化委員會主席)和Bjarne Stroustrup(C++語言的創始人)等頂級大師們的共事過程中,Stepanov開始注意到C/C++語言在實現其泛型思想方面所具有的潛在優勢。就拿C/C++中的指標而言,它的靈活與高效運用,使後來的STL在實現泛型化的同時更是保持了高效率。另外,在STL中佔據極其重要地位的迭代子概念便是源自於C/C++中原生指標(native pointer)的抽象。
1988年,Alexander Stepanov開始進入惠普的Palo Alto實驗室工作,在隨後的4年中,他從事的是有關磁碟驅動器方面的工作。直到1992年,由於參加並主持了實驗室主任Bill Worley所建立的一個有關演算法的研究專案,才使他重新回到了泛型化演算法的研究工作上來。專案自建立之後,參與者從最初的8人逐漸減少,最後只剩下兩個人--Stepanove本人和Meng Lee。經過長時間的努力,最終,信念與汗水所換來的是一個包含有大量資料結構和演算法部件的龐大執行庫。這便是現在的STL的雛形(同時也是STL的一個實現版本--HP STL)。
1993年,當時在貝爾實驗室的Andrew Koenig看到了Stepanove的研究成果,很是興奮。在他的鼓勵與幫助下,Stepanove於是年9月的聖何塞為ANSI/ISO C++標準委員會做了一個相關演講(題為"The Science of C++ Programming"),向委員們講述了其觀念。然後又於次年3月,在聖迭戈會議上,向委員會提交了一份建議書,以期使STL成為C++標準庫的一部分。儘管這一建議十分龐大,以至於降低了被通過的可能性,但由於其所包含的新思想,投票結果以壓倒多數的意見認為推遲對該建議的決定。
隨後,在眾人的幫助之下,包括Bjarne Stroustrup在內,Stepanove又對STL進行了改進。同時加入了一個封裝記憶體模式資訊的抽象模組,也就是現在STL中的allocator,它使STL的大部分實現都可以獨立於具體的記憶體模式,從而獨立於具體平臺。在同年夏季的滑鐵盧會議上,委員們以80%贊成,20%反對,最終通過了提案,決定將STL正式納入C++標準化程序之中,隨後STL便被放進了會議的工作檔案中。自此,STL終於成為了C++家族中的重要一員。
此後,隨著C++標準的不斷改進,STL也在不斷地作著相應的演化。直至1998年,ANSI/ISO C++標準正式定案,STL始終是C++標準中不可或缺的一大部件。
二、STL的框架
2.1 STL的分類
STL的程式碼從廣義上講分為三類:algorithm(演算法)、container(容器)和iterator(迭代器) 。幾乎所有的程式碼都採用了模板類 和模板函式 的方式,這相比於傳統的由函式和類組成的庫來說提供了更好的程式碼重用機會。
在C++標準中,STL被組織為下面13個頭檔案:<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack>和<utility>。
2.2 演算法
大家都能取得的一個共識是函式庫對資料型別的選擇對其可重用性起著至關重要的作用。舉例來說,一個求方根的函式,在使用浮點數作為其引數型別的情況下的可重用性肯定比使用整型作為它的引數類性要高。而C++通過模板的機制允許推遲對某些型別的選擇,直到真正想使用模板或者說對模板進行特化的時候,STL就利用了這一點提供了相當多的有用演算法。它是在一個有效的框架中完成這些演算法的——你可以將所有的型別劃分為少數的幾類,然後就可以在模版的引數中使用一種型別替換掉同一種類中的其他型別。
演算法(algorithm)是應用在容器上以各種方法處理其內容的行為和功能。例如:有對容器內容排序、複製、檢索、合併等演算法。在STL中,演算法是由模板函式表現的。這些函式不是容器類的成員函式,相反它們是獨立的函式。令人吃驚的特點之一就是其演算法是如此的通用,不僅可以將其用於STL容器,而且可以用於普通的C++陣列或任何其他應用程式指定的容器。
STL提供了大約100個實現演算法的模版函式,比如演算法for_each將為指定序列中的每一個元素呼叫指定的函式,stable_sort以你所指定的規則對序列進行穩定性排序等等。這樣一來,只要我們熟悉了STL之後,許多程式碼可以被大大的化簡,只需要通過呼叫一兩個演算法模板,就可以完成所需要的功能並大大地提升效率。
演算法部分主要由標頭檔案<algorithm>,<numeric>和<functional>組成。
- <algorithm>:是所有STL標頭檔案中最大的一個(儘管它很好理解),它是由一大堆模板函式組成的,可以認為每個函式在很大程度上都是獨立的,其中常用到的功能範圍涉及到比較、交換、查詢、遍歷操作、複製、修改、移除、反轉、排序、合併等等;
- <numeric>:體積很小,只包括幾個在序列上面進行簡單數學運算的模版函式,包括加法和乘法在序列上的一些操作;
- <functional>:其中定義了一些模板類,用以宣告函式物件。
2.3 容器
在實際的開發過程中,資料結構本身的重要性不會遜於操作於資料結構的演算法的重要性,當程式中存在著對時間要求很高的部分時,資料結構的選擇就顯得更加重要。
經典的資料結構數量有限,但是我們常常重複著一些為了實現向量、連結串列等結構而編寫的程式碼,這些程式碼都十分相似,只是為了適應不同資料的變化而在細節上有所出入。STL容器就為我們提供了這樣的方便,它允許我們重複利用已有的實現構造自己的特定型別下的資料結構,通過設定一些模版類,STL容器對最常用的資料結構提供了支援,這些模板的引數允許我們指定容器中元素的資料型別,可以將我們許多重複而乏味的工作簡化。
容器是資料在記憶體中的組織的方法,例如:陣列、堆疊、佇列、連結串列、二叉樹等(這些都不是STL標準容器)。STL中的容器是一種儲存T(Template)型別值的有限集合的資料結構,容器的內部實現一般是類。這些值可以是物件本身,如果資料型別T代表的是class的化。
容器部分主要由標頭檔案<vector>,<list>,<deque>,<set>,<map>,<stack>和<queue>組成。對於常用的一些容器和容器介面卡(可以看作由其它容器實現的容器),可以通過下表總結一下它們和相應標頭檔案的對應關係。
- 向量(vector):連續儲存的元素<vector>;
- 列表(list):由節點組成的雙向列表,每個節點包含著一個元素<list>;
- 雙佇列(deque):連續儲存的指向不同元素的指標所組成的陣列<deque>;
- 集合(set):由節點組成的紅黑樹,每個節點都包含著一個元素,節點之間以某種作用於元素對的謂詞排列,沒有兩個不同的元素能夠擁有相同的次序<set>;
- 多重集合(multiset):允許存在兩個次序相等的元素的集合<set>;
- 棧(stack):後進先出的值的排列<stack>;
- 佇列(queue):先進先出的值的排列<queue>;
- 優先佇列(priority_queue):元素的次序是由作用於所儲存的值對上的某種謂詞決定的一種佇列<queue>;
- 對映(map):由{鍵,值}對組成的集合,以某種作用於鍵對上的謂詞排列<map>;
- 多重對映(multimap):允許鍵對有相等的次序的對映<map>。
2.4 迭代器
一旦選定一種容器型別和資料行為(演算法),那麼剩下唯一要做的就是用迭代器使其相互作用。可以把迭代器看作一個指向容器中元素的普通指標,可以如遞增一個指標那樣遞增迭代器,使其依次指向容器中每一個後繼的元素。迭代器是STL中的一個關鍵部分,因為它將演算法和容器連線在一起。
下面要說的迭代器從作用上來說是最基本的部分,可是理解起來比前兩者都要費力一些(至少筆者是這樣)。軟體設計有一個基本原則,所有的問題都可以通過引進一個間接層來簡化,這種簡化在STL中就是用迭代器來完成的。
概括來說,迭代器在STL中用來將演算法和容器聯絡起來,起著一種黏和劑的作用。幾乎STL提供的所有演算法都是通過迭代器存取元素序列進行工作的,每一個容器都定義了其本身所專有的迭代器,用以存取容器中的元素。
迭代器部分主要由標頭檔案<utility>,<iterator>和<memory>組成。
- <utility>:是一個很小的標頭檔案,它包括了貫穿使用在STL中的幾個模版的宣告;
- <iterator>:提供了迭代器使用的許多方法
- <memory>:它以不同尋常的方式為容器中的元素分配儲存空間,同時也為某些演算法執行期間產生的臨時物件提供機制;<memory>中的主要部分是模板類allocator,它負責產生所有容器中的預設分配器。
對於之前不太瞭解STL的讀者來說,上面的文字只是十分概括地描述了一下STL的框架,對您理解STL的機制乃至使用STL所起到的幫助微乎甚微,這不光是因為深入STL需要對C++的高階應用有比較全面的瞭解,更因為STL的三個部分演算法、容器和迭代器三部分是互相牽制或者說是緊密結合的。從概念上講最基礎的部分是迭代器,可是直接學習迭代器會遇到許多抽象枯燥和繁瑣的細節,然而不真正理解迭代器又是無法直接進入另兩部分的學習的(至少對剖析原始碼來說是這樣)。可以說,適應STL處理問題的方法是需要花費一定的時間的,但是以此為代價,STL取得了一種十分可貴的獨立性,它通過迭代器能在儘可能少地知道某種資料結構的情況下完成對這一結構的運算,所以下決心鑽研STL的朋友們千萬不要被一時的困難擊倒。其實STL運用的模式相對統一,只要適應了它,從一個STL工具到另一個工具,都不會有什麼大的變化。
對於STL的使用,也普遍存在著兩種觀點。第一種認為STL的最大作用在於充當經典的資料結構和演算法教材,因為它的原始碼涉及了許多具體實現方面的問題。第二種則認為STL的初衷乃是為了簡化設計,避免重複勞動,提高程式設計效率,因此應該是“應用至上”的,對於原始碼則不必深究。筆者則認為分析原始碼和應用並不矛盾,通過分析原始碼也能提高我們對其應用的理解,當然根據具體的目的也可以有不同的側重。
三、50條忠告
- 把C++當成一門新的語言學習;
- 看《Thinking In C++》,不要看《C++變成死相》;
- 看《The C++ Programming Language》和《Inside The C++ Object Model》,不要因為他們很難而我們自己是初學者所以就不看;
- 不要被VC、BCB、BC、MV、TC等詞彙所迷惑——他們都是整合開發環境,而我們要學的是一門語言;
- 不要放過任何一個看上去很簡單的小程式設計問題——他們往往並不那麼簡單,或者可以引申出很多知識點;
- 會用Visual C++,並不說明你就會C++;
- 學class並不難,template、STL、generic programming也不過如此——難的是長期堅持實踐和不遺餘力的博覽群書;
- 如果不是天才的話,想學程式設計就不要想玩遊戲——你以為你做到了,其實你的C++水平並沒有和你通關的能力一起變高——其實可以時刻記住:學習C++是為了編遊戲的;
- 看Visual C++的樹,是學不了C++語言的;
- 把時髦的技術掛在嘴邊,還不如把過時的技術記在心裡;
- 學習程式設計最好的方法之一就是閱讀原始碼;
- 在任何時刻都不要認為自己手中的書已經足夠了;
- 請閱讀《The Standard C++ Bible》(中文版:標準C++寶典),掌握C++標準;
- 看得懂的書,請仔細看;看不懂的書,請硬著頭皮看;
- 別指望看一遍書就能記住和掌握什麼——請看第二遍、第三遍。。。;
- 請看《Effective C++》和《More Effective C++》以及《Exceptional C++》;
- 不要停留在整合開發環境的搖籃上,要學會控制整合開發環境,還要學會用命令列方式處理程式;
- 和別人一起討論有意義的C++知識點,而不是真吵XX行不行或者YY與ZZ哪個好;
- 請看《程式設計實踐》,並嚴格的按照其要求去做;
- 不要因為C和C++中有一些語法和關鍵字看上去相同,就認為它們的意義和作用完全一樣;
- C++絕不是所謂的C的“擴充”——如果C++一開始就起名叫Z語言,你一定不會把C和Z語言聯絡得那麼緊密;
- 請不要認為學過XX語言再改學C++會有什麼問題——你只不過又在學一門全新的語言而已;
- 讀完了《Inside The C++ Object Model》以後再來認定自己是不是已經學會了C++;
- 學習程式設計的祕訣是:程式設計、程式設計、再程式設計;
- 請留意下列書籍:《C++面向物件高效程式設計(C++ Effective Object-Oriented Software Construction)》、《面向物件軟體構造(Object-Oriented Software Construction)》、《設計模式(Design Patterns)》、《The Art of Computer Programming》;
- 請把書上的程式例子親手輸入到電腦上實踐,即使配套光碟中有原始碼;
- 把在書上看到的有意義的例子擴充;
- 請重視C++中的異常處理技術,並將其切實的運用到自己的程式中;
- 經常回顧自己以前寫過的程式,並嘗試重寫,把自己學到的新知識運用進去;
- 不要漏掉書中任何一個練習題——請全部做完並記錄下解題思路;
- C++語言和C++的整合開發環境要同時學習和掌握;
- 既然決定了學習C++,就請堅持下去,因為學習程式設計語言的目的是掌握程式設計技術,而程式設計技術是跨語言的;
- 就讓C++語言的各種平臺和開發環境去激烈競爭把,我們要以學習C++語言本身為主;
- 當你寫C++程式寫到一半卻發現自己用的方法很拙劣時,請不要馬上停手;請儘快將餘下的部分初略的完成以保證這個設計的完整性,然後分析自己的錯誤並重新設計和編寫;
- 別心急,設計C++的class確實不容易;自己程式中的class和自己的class設計水平是在不斷的程式設計實踐中完善和發展的;
- 決不要因為程式“很小”就不遵循某些你不熟悉的規則——好習慣是培養出來的,而不是一次性記住的;
- 每學到一個C++難點的時候,嘗試著對別人講解這個知識點並讓他理解——你能講清楚才說明你真的理解了;
- 記錄下在和別人交流時發現的自己忽視或不理解的知識點;
- 請不斷的對自己寫的程式提出更高的要求,哪怕你的程式版本號會變成Version 100.xxx;
- 儲存好你寫過的所有的程式——那是你最好的積累之一;
- 請不要做浮躁的人;
- 請熱愛C++。
四、C++標頭檔案
4.1 傳統C++
#include <assert.h> 設定插入點 #include <ctype.h> 字元處理 #include <errno.h> 定義錯誤碼 #include <float.h> 浮點數處理 #include <fstream.h> 檔案輸入/輸出 #include <iomanip.h> 引數化輸入/輸出 #include <iostream.h> 資料流輸入/輸出 #include <limits.h> 定義各種資料型別最值常量 #include <locale.h> 定義本地化函式 #include <math.h> 定義數學函式 #include <stdio.h> 定義輸入/輸出函式 #include <stdlib.h> 定義雜項函式及記憶體分配函式 #include <string.h> 字串處理 #include <strstrea.h> 基於陣列的輸入/輸出 #include <time.h> 定義關於時間的函式 include <wchar.h> 寬字元處理及輸入/輸出 include <wctype.h> 寬字元分類
4.2 標準 C++
#include <algorithm> 通用演算法 #include <bitset> 位集容器 #include <cctype> #include <cerrno> #include <clocale> #include <cmath> #include <complex> 複數類 #include <cstdio> #include <cstdlib> #include <cstring> #include <ctime> #include <deque> 雙端佇列容器 #include <exception> 異常處理類 #include <fstream> #include <functional> 定義運算函式(代替運算子) #include <limits> #include <list> 線性列表容器 #include <map> 對映容器 #include <iomanip> #include <ios> 基本輸入/輸出支援 #include <iosfwd> 輸入/輸出系統使用的前置宣告 #include <iostream> #include <istream> 基本輸入流 #include <ostream> 基本輸出流 #include <queue> 佇列容器 #include <set> 集合容器 #include <sstream> 基於字串的流 #include <stack> 堆疊容器 #include <stdexcept> 標準異常類 #include <streambuf> 底層輸入/輸出支援 #include <string> 字串類 #include <utility> 通用模板類 #include <vector> 動態陣列容器 #include <cwchar> #include <cwctype>
4.3 C99增加
#include <complex.h> 複數處理 #include <fenv.h> 浮點環境 #include <inttypes.h> 整數格式轉換 #include <stdbool.h> 布林環境 #include <stdint.h> 整型環境 #include <tgmath.h> 通用型別數學巨集