STL 簡介,標準模板庫
作者:Scott Field
這篇文章是關于C++語言的一個新的擴展——標準模板庫的(Standard Template Library),也叫STL。 當我第一次打算寫一篇關于STL的文章的時候,我不得不承認我當時低估了這個話題的深度和廣度。有很多內容要含蓋,也有很多詳細描述STL的書。因此我重新考慮了一下我原來的想法。我為什么要寫這篇文章,又為什么要投稿呢?這會有什麼用呢?有再來一篇關于STL的文章的必要嗎?
當我翻開Musser and Saini的頁時,我看到了編程時代在我面前消融。我能看到深夜消失了, 目標軟件工程出現了。我看到了可維護的代碼。一年過去了,我使用STL寫的軟件仍然很容易維護。 讓人吃驚的是其他人可以沒有我而維護的很好!
然而,我也記得在一開始的時候很難弄懂那些技術術語。一次,我買了Musser&Saini,每件事都依次出現,但是在那以前我最渴望得到的東西是一些好的例子。
當我開始的時候,作為C++一部分的Stroustrup還沒出來,它覆蓋了STL。
因此我想寫一篇關于一個STL程序員的真實生活的文章可能會有用。如果我手上有一些好的例子的話,特別是象這樣的新題目,我會學的更快。
另外一件事是STL應該很好用。因此,理論上說,我們應該可以馬上開始使用STL。
什麼是STL呢?STL就是Standard Template Library,標準模板庫。這可能是一個歷史上最令人興奮的工具的最無聊的術語。從根本上說,STL是一些“容器”的集合,這些“容器”有list,vector,set,map等,STL也是算法和其他一些組件的集合。這里的“容器”和算法的集合指的是世界上很多聰明人很多年的杰作。
STL的目的是標準化組件,這樣你就不用重新開發它們了。你可以僅僅使用這些現成的組件。STL現在是C++的一部分,因此不用額外安裝什麼。它被內建在你的編譯器之內。因為STL的list是一個簡單的容器,所以我打算從它開始介紹STL如何使用。如果你懂得了這個概念,其他的就都沒有問題了。另外,list容器是相當簡單的,我們會看到這一點。
這篇文章中我們將會看到如何定義和初始化一個list,計算它的元素的數量,從一個list里查找元素,刪除元素,和一些其他的操作。要作到這些,我們將會討論兩個不同的算法,STL通用算法都是可以操作不止一個容器的,而list的成員函數是list容器專有的操作。
這是三類主要的STL組件的簡明綱要。STL容器可以保存對象,內建對象和類對象。它們會安全的保存對象,并定義我們能夠操作的這個對象的接口。放在蛋架上的雞蛋不會滾到桌上。它們很安全。因此,在STL容器中的對象也很安全。我知道這個比喻聽起來很老土,但是它很正確。
STL算法是標準算法,我們可以把它們應用在那些容器中的對象上。這些算法都有很著名的執行特性。它們可以給對象排序,刪除它們,給它們記數,比較,找出特殊的對象,把它們合并到另一個容器中,以及執行其他有用的操作。
STL iterator就象是容器中指向對象的指針。STL的算法使用iterator在容器上進行操作。Iterator設置算法的邊界 ,容器的長度,和其他一些事情。舉個例子,有些iterator僅讓算法讀元素,有一些讓算法寫元素,有一些則兩者都行。 Iterator也決定在容器中處理的方向。
你可以通過調用容器的成員函數begin()來得到一個指向一個容器起始位置的iterator。你可以調用一個容器的 end() 函數來得到過去的最后一個值(就是處理停在那的那個值)。
這就是STL所有的東西,容器、算法、和允許算法工作在容器中的元素上的iterator。 算法以合適、標準的方法操作對象,并可通過iterator得到容器精確的長度。一旦做了這些,它們就在也不會“跑出邊界”。 還有一些其他的對這些核心組件類型有功能性增強的組件,例如函數對象。我們將會看到有關這些的例子,現在 ,我們先來看一看STL的list。
--------------------------------------------------------------------------------
定義一個list 我們可以象這樣來定義一個STL的list: #include <string> #include <list> int main (void) { list<string> Milkshakes; } 這就行了,你已經定義了一個list。簡單嗎?list<string> Milkshakes這句是你聲明了list<string>模板類 的一個實例,然后就是實例化這個類的一個對象。但是我們別急著做這個。在這一步其實你只需要知道你定義了 一個字符串的list。你需要包含提供STL list類的頭文件。我用gclearcase/" target="_blank" >cc 2.7.2在我的Linux上編譯這個測試程序,例如: g++ test1.cpp -otest1 注意iostream.h這個頭文件已經被STL的頭文件放棄了。這就是為什么這個例子中沒有它的原因。 現在我們有了一個list,我們可以看實使用它來裝東西了。我們將把一個字符串加到這個list里。有一個非常 重要的東西叫做list的值類型。值類型就是list中的對象的類型。在這個例子中,這個list的值類型就是字符串,string , 這是因為這個list用來放字符串。
--------------------------------------------------------------------------------
I使用list的成員函數push_back和push_front插入一個元素到list中 #include <string> #include <list> # int main (void) { list<string> Milkshakes; Milkshakes.push_back("Chocolate"); Milkshakes.push_back("Strawberry"); Milkshakes.push_front("Lime"); Milkshakes.push_front("Vanilla"); } We now have a list with four strings in it. The list member function push_back() places an object onto the back of the list. The list member function push_front() puts one on the front. I often push_back() some error messages onto a list, and then push_front() a title on the list so it prints before the error messages. 我們現在有個4個字符串在list中。list的成員函數push_back()把一個對象放到一個list的后面,而 push_front()把對象放到前面。我通常把一些錯誤信息push_back()到一個list中去,然后push_front()一個標題到list中, 這樣它就會在這個錯誤消息以前打印它了。
--------------------------------------------------------------------------------
The list member function empty()list的成員函數empty() 知道一個list是否為空很重要。如果list為空,empty()這個成員函數返回真。 我通常會這樣使用它。通篇程序我都用push_back()來把錯誤消息放到list中去。然后,通過調用empty() 我就可以說出這個程序是否報告了錯誤。如果我定義了一個list來放信息,一個放警告,一個放嚴重錯誤, 我就可以通過使用empty()輕易的說出到底有那種類型的錯誤發生了。 我可以整理這些list,然后在打印它們之前,用標題來整理它們,或者把它們排序成類。
這是我的意思:
/* || Using a list to track and report program messages and status */ #include <iostream.h> #include <string> #include <list> # int main (void) { #define OK 0 #define INFO 1 #define WARNING 2 # int return_code; # list<string> InfoMessages; list<:string> WarningMessages; # // during a program these messages are loaded at various points InfoMessages.push_back("Info: Program started"); // do work... WarningMessages.push_back("Warning: No Customer records have been found"); // do work... # return_code = OK; # if (!InfoMessages.empty()) { // there were info messages InfoMessages.push_front("Informational Messages:"); // ... print the info messages list, we´ll see how later return_code = INFO; } # if (!WarningMessages.empty()) { // there were warning messages WarningMessages.push_front("Warning Messages:"); // ... print the warning messages list, we´ll see how later return_code = WARNING; } # // If there were no messages say so. if (InfoMessages.empty() && WarningMessages.empty()) { cout << "There were no messages " << endl; } # return return_code; }
--------------------------------------------------------------------------------
用for循環來處理list中的元素 我們想要遍歷一個list,比如打印一個中的所有對象來看看list上不同操作的結果。要一個元素一個元素的遍歷一個list, 我們可以這樣做: /* || How to print the contents of a simple STL list. Whew! */ #include <iostream.h> #include <string> #include <list> # int main (void) { list<string> Milkshakes; list<string>::iterator MilkshakeIterator; # Milkshakes.push_back("Chocolate"); Milkshakes.push_back("Strawberry"); Milkshakes.push_front("Lime"); Milkshakes.push_front("Vanilla"); # // print the milkshakes Milkshakes.push_front("The Milkshake Menu"); Milkshakes.push_back("*** Thats the end ***"); for (MilkshakeIterator=Milkshakes.begin(); MilkshakeIterator!=Milkshakes.end(); ++MilkshakeIterator) { // dereference the iterator to get the element cout << *MilkshakeIterator << endl; } } 這個程序定義了一個iterator,MilkshakeIterator。我們把它指向了這個list的第一個元素。 這可以調用Milkshakes.begin()來作到,它會返回一個指向list開頭的iterator。然后我們把它和Milkshakes.end()的 返回值來做比較,當我們到了那兒的時候就停下來。 容器的end()函數會返回一個指向容器的最后一個位置的iterator。當我們到了那里,就停止操作。 我們不能不理容器的end()函數的返回值。我們僅知道它意味著已經處理到了這個容器的末尾,應該停止處理了。 所有的STL容器都要這樣做。
在上面的例子中,每一次執行for循環,我們就重復引用iterator來得到我們打印的字符串。
在STL編程中,我們在每個算法中都使用一個或多個iterator。我們使用它們來存取容器中的對象。 要存取一個給定的對象,我們把一個iterator指向它,然后間接引用這個iterator。
這個list容器,就象你所想的,它不支持在iterator加一個數來指向隔一個的對象。 就是說,我們不能用Milkshakes.begin()+2來指向list中的第三個對象,因為STL的list是以雙鏈的list來實現的, 它不支持隨機存取。vector和deque(向量和雙端隊列)和一些其他的STL的容器可以支持隨機存取。
上面的程序打印出了list中的內容。任何人讀了它都能馬上明白它是怎麼工作的。它使用標準的iterator和標準 的list容器。沒有多少程序員依賴它里面裝的東西, 僅僅是標準的C++。這是一個向前的重要步驟。這個例子使用STL使我們的軟件更加標準。
--------------------------------------------------------------------------------
用STL的通用算法for_each來處理list中的元素 使用STL list和 iterator,我們要初始化、比較和給iterator增量來遍歷這個容器。STL通用的for_each 算法能夠減輕我們的工作。 /* || How to print a simple STL list MkII */ #include <iostream.h> #include <string> #include <list> #include <algorithm> # PrintIt (string& StringToPrint) { cout << StringToPrint << endl; } # int main (void) { list<string> FruitAndVegetables; FruitAndVegetables.push_back("carrot"); FruitAndVegetables.push_back("pumpkin"); FruitAndVegetables.push_back("potato"); FruitAndVegetables.push_front("apple"); FruitAndVegetables.push_front("pineapple"); # for_each (FruitAndVegetables.begin(), FruitAndVegetables.end(), PrintIt); } 在這個程序中我們使用STL的通用算法for_each()來遍歷一個iterator的范圍,然后調用PrintIt()來處理每個對象。 我們不需要初始化、比較和給iterator增量。for_each()為我們漂亮的完成了這些工作。我們執行于對象上的 操作被很好的打包在這個函數以外了,我們不用再做那樣的循環了,我們的代碼更加清晰了。 for_each算法引用了iterator范圍的概念,這是一個由起始iterator和一個末尾iterator指出的范圍。 起始iterator指出操作由哪里開始,末尾iterator指明到哪結束,但是它不包括在這個范圍內。
--------------------------------------------------------------------------------
用STL的通用算法count()來統計list中的元素個數。 STL的通用算法count()和count_it()用來給容器中的對象記數。就象for_each()一樣,count()和count_if() 算法也是在iterator范圍內來做的。 讓我們在一個學生測驗成績的list中來數一數滿分的個數。這是一個整型的List。
/* || How to count objects in an STL list */ #include <list> #include <algorithm> # int main (void) { list<int> Scores; # Scores.push_back(100); Scores.push_back(80); Scores.push_back(45); Scores.push_back(75); Scores.push_back(99); Scores.push_back(100); # int NumberOf100Scores(0); count (Scores.begin(), Scores.end(), 100, NumberOf100Scores); # cout << "There were " << NumberOf100Scores << " scores of 100" << endl; } The count() algorithm counts the number of objects equal to a certain value. In the above example it checks each integer object in a list against 100. It increments the variable NumberOf100Scores each time a container object equals 100. The output of the program is count()算法統計等于某個值的對象的個數。上面的例子它檢查list中的每個整型對象是不是100。每次容器中的對象等于100,它就給NumberOf100Scores加1。這是程序的輸出: There were 2 scores of 100
--------------------------------------------------------------------------------
用STL的通用算法count_if()來統計list中的元素個數 count_if()是count()的一個更有趣的版本。他采用了STL的一個新組件,函數對象。count_if() 帶一個函數對象的參數。函數對象是一個至少帶有一個operator()方法的類。有些STL算法作為參數接收 函數對象并調用這個函數對象的operator()方法。 函數對象被約定為STL算法調用operator時返回true或false。它們根據這個來判定這個函數。舉個例子會 說的更清楚些。count_if()通過傳遞一個函數對象來作出比count()更加復雜的評估以確定一個對象是否應該被 記數。在這個例子里我們將數一數牙刷的銷售數量。我們將提交包含四個字符的銷售碼和產品說明的銷售記錄。
/* || Using a function object to help count things */ #include <string> #include <list> #include <algorithm> # const string ToothbrushCode("0003"); # class IsAToothbrush { public: bool operator() ( string& SalesRecord ) { return SalesRecord.substr(0,4)==ToothbrushCode; } }; # int main (void) { list<string> SalesRecords; # SalesRecords.push_back("0001 Soap"); SalesRecords.push_back("0002 Shampoo"); SalesRecords.push_back("0003 Toothbrush"); SalesRecords.push_back("0004 Toothpaste"); SalesRecords.push_back("0003 Toothbrush"); # int NumberOfToothbrushes(0); count_if (SalesRecords.begin(), SalesRecords.end(), IsAToothbrush(), NumberOfToothbrushes); # cout << "There were " << NumberOfToothbrushes << " toothbrushes sold" << endl; } 這是這個程序的輸出: There were 2 toothbrushes sold(一共賣了兩把牙刷) 這個程序是這樣工作的:定義一個函數對象類IsAToothbrush,這個類的對象能判斷出賣出的是否是牙刷 。如果這個記錄是賣出牙刷的記錄的話,函數調用operator()返回一個true,否則返回false。 count_if()算法由第一和第二兩個iterator參數指出的范圍來處理容器對象。它將對每個 IsAToothbrush()返回true的容器中的對象增加NumberOfToothbrushes的值。
最后的結果是NumberOfToothbrushes這個變量保存了產品代碼域為"0003"的記錄的個數,也就是牙刷的個數。
注意count_if()的第三個參數IsAToothbrush(),它是由它的構造函數臨時構造的一個對象。你可以把IsAToothbrush類的一個臨時對象 傳遞給count_if()函數。count_if()將對該容器的每個對象調用這個函數。
--------------------------------------------------------------------------------
使用count_if()的一個更加復雜的函數對象。 我們可以更進一步的研究一下函數對象。假設我們需要傳遞更多的信息給一個函數對象。我們不能通過 調用operator來作到這點,因為必須定義為一個list的中的對象的類型。 然而我們通過為IsAToothbrush指出一個非缺省的構造函數就可以用任何我們所需要的信息來初始化它了。 例如,我們可能需要每個牙刷有一個不定的代碼。我們可以把這個信息加到下面的函數對象中: /* || Using a more complex function object */ #include <iostream.h> #include <string> #include <list> #include <algorithm> # class IsAToothbrush { public: IsAToothbrush(string& InToothbrushCode) : ToothbrushCode(InToothbrushCode) {} bool operator() (string& SalesRecord) { return SalesRecord.substr(0,4)==ToothbrushCode; } private: string ToothbrushCode; }; # int main (void) { list<string> SalesRecords; # SalesRecords.push_back("0001 Soap"); SalesRecords.push_back("0002 Shampoo"); SalesRecords.push_back("0003 Toothbrush"); SalesRecords.push_back("0004 Toothpaste"); SalesRecords.push_back("0003 Toothbrush"); # string VariableToothbrushCode("0003"); # int NumberOfToothbrushes(0); count_if (SalesRecords.begin(), SalesRecords.end(), IsAToothbrush(VariableToothbrushCode), NumberOfToothbrushes); cout << "There were " << NumberOfToothbrushes << " toothbrushes matching code " << VariableToothbrushCode << " sold" << endl; } 程序的輸出是: There were 2 toothbrushes matching code 0003 sold 這個例子演示了如何向函數對象傳遞信息。你可以定義任意你想要的構造函數,你可以再函數對象中做任何你 想做的處理,都可以合法編譯通過。 你可以看到函數對象真的擴展了基本記數算法。
到現在為止,我們都學習了:
定義一個list 向list中加入元素 如何知道list是否為空 如何使用for循環來遍歷一個list 如何使用STL的通用算法for_each來遍歷list list成員函數begin() 和 end() 以及它們的意義 iterator范圍的概念和一個范圍的最后一個位置實際上并不被處理這一事實 如何使用STL通用算法count()和count_if()來對一個list中的對象記數 如何定義一個函數對象 我選用這些例子來演示list的一般操作。如果你懂了這些基本原理,你就可以毫無疑問的使用STL了 建議你作一些練習。我們現在用一些更加復雜的操作來擴展我們的知識,包括list成員函數和STL通用算法。
|