第十六章 string類和標準模板庫(4)泛型編程

(四)泛型編程

STL是一種泛型編程,面向對象的編程關注的是數據結構,而泛型編程關注的是算法。它們的共同點是抽象和創建可重用代碼。

1.迭代器

基於算法的要求,來設計基本迭代器的特徵和基本容器的特徵。容器類模板是將算法獨立於存儲的數據類型,而迭代器是將算法獨立於各種容器類模板,使得某種算法可以使用各容器類的迭代器用相同的方式來處理不同的容器類型。模板提供了存儲在容器中的數據類型的通用表示,使得算法獨立於不同的數據類型,而迭代器則提供了遍歷不同容器類數據的通用表示,使得算法獨立於不同容器(比如數組和鏈表)的具體的數據結構。

對於普通的數組來說,相應類型的數組指針就可以作爲迭代器,而對於更一般的類對象來說,迭代器也是相應的類對象(但是具有與指針相似的性質),可以對迭代器使用解除引用運算符(解除引用後就是相應的存儲的數據)以及++運算符等(這些運算符在迭代器類中進行了重載),一般來說,每個容器類都定義了自己的迭代器,而且迭代器的類型各不相同,爲的是可以實現不同的功能。

爲了區分++運算符的前綴版本和後綴版本,c++中規定了將operator++作爲前綴版本,將operator++(int)作爲後綴版本(括號中的參數永遠不會用到,只是後綴版本的一種標識,因此不必有變量名稱),前綴版本是先加後返回加了之後的值,而後綴版本是返回加之前的值,並進行++操作。

爲了能夠統一不同的容器類,c++規定迭代器都有起始迭代器和超尾迭代器(超尾標記),比如鏈表容器類就要有個沒有數據的超尾結點。begin()和end()就是指向起始位置和超尾位置的迭代器,我們無需知道超尾是如何實現的,也無需知道迭代器是如何實現的,我們僅僅知道迭代器的通用方法即可。注意,一個類的迭代器,我們僅需要這樣使用,比如vector<double>::iterator pp;就是一個指向vector<double>類對象的迭代器,其他模板類也是如此。

2.迭代器的類型

輸入迭代器:這個輸入是對程序而言的,即從容器中讀取數據,解除引用可以讓迭代器得到容器的數據。輸入迭代器是單向迭代器,可以遞增,但不可以倒退。輸入迭代器還是單通行的,也就是說不依賴於前一次遍歷的值也不依賴於本次已經遍歷的值。

輸出迭代器:表示程序用這個迭代器將內容輸出到容器(也是對程序而言的),對其解除引用可以讓程序修改容器值,而不是讀取。簡而言之,對於單通行,只讀算法,可以使用輸入迭代器;而對於單通行,只寫算法,可以使用輸出迭代器。

正向迭代器:和輸入迭代器與輸出迭代器相同,正向迭代器只使用++運算符來遍歷容器,正向迭代器可以讀取也可以寫入,如果使用const修飾可以設置成只能讀取。正向迭代器就是輸入迭代器和輸出迭代器的結合體,並且正向迭代器是多次通行的。

雙向迭代器:可以雙向遍歷容器,比如前一個遞加,後一個遞減。

隨機訪問迭代器:也就是可以跳到容器中的任何一個元素,上面的迭代器可以遞加遞減等,但不可以加上一個整型,也就是隨機訪問。這裏的隨機並不是指隨機指向一個元素,而是我希望指向哪個元素都可以立即指向它,也就是直接跳到容器的任何一個元素,這就叫作隨機訪問。隨機訪問迭代器支持上面的雙向迭代器的所有功能,同時還支持重載的加法運算,並且迭代器可以進行比較運算。

3.迭代器的層次結構

高層次的迭代器擁有低層次迭代器的全部功能,輸入和輸出迭代器是最低層次的迭代器;再是正向迭代器;再是雙向迭代器;再是隨機訪問迭代器。一般我們可以直接使用隨機訪問迭代器,只有在特定的具有安全性要求的場合下我們才使用特定類型的迭代器。隨機訪問迭代器層次下的迭代器沒有[]運算和加法減法等運算。

4.概念,改進和模型

各種迭代器的類型只是一種概念性的描述,並不是目前已經實現和存在的,比如list<double> a;就是一個雙向迭代器,而vector<double> b;就是一個隨機訪問迭代器。

上面講到的迭代器是更多的是一種要求,而不是類型,這種要求就是概念。概念的類似繼承的關係叫作改進,比如雙向迭代器就是正向迭代器的改進;概念的具體化叫作模型。比如指向一個int類型的常規指針就是一個隨機訪問迭代器的模型。

可以將STL算法用於常規數組,因爲數組指針可以看成是一種隨機訪問迭代器。STL提供了一組預定義的迭代器,比如copy(a,b,c);函數就可以將迭代器a,b的內容複製到c迭代器位置,也就是將一個容器的內容複製到另一個容器的相應位置;使用這種copy的方法,我們可以將容器的內容輸出到輸出流中,首先我們要定義一個輸出流的迭代器(或者可以被稱爲適配器),比如可以包含文件:

#include <iterator>

ostream_iterator<int,char> out_iter(cout,””);//定義輸出流迭代器,輸出類型爲int,輸出的字符類型爲char,輸出以空格分隔,使用cout輸出到屏幕

copy(dice.begin(),dice.end(),out_iter);

這種方法可以使容器類的輸入輸出更爲方便。

5.其他有用的迭代器

c++的STL中,迭代器也是一種類模板,比如istream_iterator<int,char> a(cout,”;”);這種輸出流迭代器。STL提供了一系列的迭代器來方便對數據的處理和輸入輸出等操作。除了istream_iterator和ostream_iterator迭代器之外,還有reverse_iterator,back_insert_iterator,front_insert_iterator和insert_iterator。

reverse_iterator:對reverse_iteratora執行遞增操作導致它遞減,這主要是爲了簡化已有的函數(通過這種反轉的迭代器可以使遞增輸出的函數來遞減輸出)。比如vector模板類中有一個rebegin()和rend()的函數,這兩個函數分別返回超尾迭代器和指向第一個元素的迭代器,但是都是reverse_iterator類型的迭代器。因爲對反向迭代器的遞增操作就是遞減,因此可以使用如下的方法來反向顯示容器的內容:copy(dice.rebegin(),rend(),out_iter);這樣甚至不必聲明反向迭代器。這裏要注意的是對於反向迭代器來說,對它使用解除引用,相當於對它遞減1之後再使用解除引用(本質是指向它的前一個元素),這就是反向迭代器的特殊補償。

back_insert_iterator,會將內容插入到容器的尾部;front_insert_iterator,會將內容插入到容器的頭部;insert_iterator,可以將內容插入到容器的構造參數位置的前面,它有兩個構造參數,一個是容器的名稱,另一個是指向容器的要在前面插入內容的元素的迭代器。這三個迭代器都是輸出容器迭代器的概念模型(輸出迭代器就是隻寫)。使用方法:要爲名爲dice的vector<double>容器創建一個back_insert_iterator迭代器,可以使用如下的方式,back_insert_iterator<vector<double>  > back_iter(dice);,可以看出,迭代器也是一種模板,需要使用具體化的容器模板來對它進行具體化,而構造函數的參數就是具體的容器對象。這個迭代器模板是在<interator>中定義的,而一般的迭代器是在相應的容器的模板文件中定義的。必須使用容器類型來進行聲明的原因是迭代器必須使用合適的容器類的方法。

6.容器種類

(1)容器概念:容器是存儲對象的對象。被存儲的對象必須是同一種類型的,可以是內置數據類型,也可以是OOP意義上的對象。不是任意類型的對象都可以添加到容器中的,只有那些具有複製構造函數和可賦值的類對象纔可以添加到容器中(也就是複製構造函數和賦值運算符都是公有的)。

(2)容器要求:複雜度要求(線性複雜度,固定複雜度)(編譯時間就是在編譯的時候已經計算了所有的計算量);返回類型要求。

複製構造和複製賦值以及移動構造和移動賦值之間的差別主要是:複製操作保留源對象,而移動操作可能修改原對象,還可以轉讓所有權而不做任何複製。通常移動操作的效率要高於複製操作。

(3)序列:可以通過添加要求來改進基本的容器概念,序列是一種重要的改進。序列中的元素具有特定的順序(這裏的順序並不是說要由小到大,而是具有固定的不變的順序)。

(4)有七種序列容器類型,下面加以介紹。

Vector:除序列外,vector還是一個可反轉容器,vector模板類是最簡單的序列類型,除非其他序列的優點能更好滿足程序的要求,否則我們優先選取vector模板類來構造對象

Deque類:在頭文件<deque>中定義,在頭文件中定義,表示雙端隊列,類似於vector容器,但與vector類相比不同點是,從起始位置執行插入和刪除操作的時間複雜度也是固定時間,而不是像vector類那樣從結尾處是固定時間,從開頭和中間是線性時間。

List類:表示雙向鏈表。與vector的區別是,所有的節點的插入與刪除的時間都是固定時間。而vector類只有在結尾處是固定時間的插入,但是在開頭和中間是線性時間的插入。因此,vector強調的是通過隨機訪問進行快速訪問,而list類(雙向鏈表)強調的是元素的快速插入和刪除(list雙向鏈表在任何位置的插入和刪除都是固定時間)。list的典型的成員函數:sort()排序,splice(iterator pos,list<T,Alloc> x)將鏈表x插入到迭代器pos之前,並且是移動插入(不改變指向x的迭代器),unique()函數,就是將連續的相同的元素合併成一個。需要注意的是非成員的函數sort,因爲sort函數需要隨機訪問迭代器,而鏈表可以執行快速插入的代價就是放棄隨機訪問,因此不能將鏈表應用於非成員的sort函數。

Foward_list類:每個節點只連接到下一個節點,而沒有鏈接到上一個節點,因此是不可反轉的容器。

Queue容器類:隊列,功能更少,甚至不能遍歷容器。它是一個適配器接口,功能是讓底層類,默認爲讓deque展示典型的隊列接口。也即是隊列只能進行從結尾加,從開頭刪,是否非空等操作。

Priority_queue類:也是在queue頭文件中聲明的。默認的底層類是vector,功能與queue差不多,不同點或者說最大特點是最大的元素被移到隊首(比如隊列中的vip用戶要優先服務)。使用方法:priority_queue<int> a;或者priority_queue<int> b(greater<int>);其中第二個構造函數是通過一個函數對象來進行初始化,裏面的greater<int>()是一個預定義的函數對象,greater是一個函數對象類模板。

Stack:也是一個適配器類,它給底層類(我的理解是繼承關係),默認爲vector類提供了典型的棧接口。方法有壓入,彈出,查看棧頂值(不能進行遍歷,也就是不能查找,只能查看棧頂這元素),檢查元素數目和檢查是否爲空等。函數分別是push,pop,top,size,empty等。

Array類:在頭文件<array>中定義的,它並非STL容器,因爲它的長度是固定的,也因此不能使用push_back和insert等調整容器大小的操作。但是它定義了operator []和at()函數,並且可以將很多標準STL算法用於對象,比如for_each()和copy()。

6.關聯容器和無序關聯容器

關聯容器是對容器概念的另一個改進,關聯容器將值與鍵關聯在一起,可以用鍵來查看值。通常,對於一個容器X來說,X::value_type通常表示容器中儲存的值的類型,對於關聯容器來說,表達式X::key_type表示容器中的鍵的類型。關聯容器的優點在於快速的檢索信息,一般來說,關聯容器有快速確定數據位置的算法,以便可以快速的插入和檢索信息。關聯容器通常是使用某種樹來實現的(也就是數據結構中的樹或二叉數的邏輯形式)。

STL提供有四種典型的關聯容器:set,multiset,map,multimap這四種。set和multiset是在頭文件<set>中定義的,而map和multimap是在頭文件<map>中定義的。set是鍵和值的類型相同的一種關聯容器,而multiset鍵和值的類型也是相同的,但是值可以有多個。同樣,map是鍵和值類型不同的關聯容器,值只有一個,multimap值可以有多個,鍵只能有一個。

set不能存儲多個相同的值,因爲它的鍵和值其實是一樣的,而鍵只能有一個,是唯一的。set的模板構造函數和vector,list相似,都是用存儲的類型來進行模板具體化,還有另外一個模板參數,就是指定用來進行比較的函數,默認就是less<>模板函數,如set<string,less<> > A;。set的構造函數也可以是兩個迭代器,第一個指向第一個元素,第二個指向超尾元素,set的構造的對象會將相同的元素合併成一個,然後會將元素按照特定的規則排序(默認是less<>模板函數)。

無序關聯容器是對容器概念的另一種改進,無序關聯容器也將值和鍵連接在一起,並使用鍵來查找值。二者的區別是,關聯容器是基於樹結構的,而無序關聯容器是基於數據結構哈希表的。旨在提高添加和刪除元素的速度以及提高查找算法的效率。四種無序關聯容器是:unordered_set,unordered_multiset,unordered_map,unordered_multimap。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章