專欄簡介:本專欄主要面向C++初學者,解釋C++的一些基本概念和基礎語言特性,涉及C++標準庫的用法,面向對象特性,泛型特性高級用法。通過使用標準庫中定義的抽象設施,使你更加適應高級程序設計技術。希望對讀者有幫助!
目錄
- 10.4再探迭代器
- 插人迭代器
- iostream迭代器
- 使用算法操作流迭代器
- ostream_iterator操作
- 使用流迭代器處理類類型
- 反向迭代器
- 反向迭代器需要遞減運算符
10.4再探迭代器
除了為每個容器定義的迭代器之外,標準庫在頭文件iterator中還定義了額外幾種迭代器。這些迭代器包括以下幾種。
- 插入迭代器(insert iterator):這些迭代器被綁定到一個容器上,可用來向容器插入元素。
- 流迭代器(stream iterator):這些迭代器被綁定到輸入或輸出流上,可用來遺歷所關聯的IO流。
- 反向迭代器(reverse iterator):這些迭代器向后而不是向前移動。除了forward_1ist之外的標準庫容器都有反向迭代器。
- 移動迭代器(move iterator):這些專用的迭代器不是拷貝其中的元素,而是移動它們。
插人迭代器
插入器是一種迭代器適配器,它接受一個容器,生成一個追代器,能實現向給定容器添加元素。當我們通過一個插入迭代器進行賦值時,該迭代器調用容器操作來向給定容器的指定位置插入一個元素。表10.2列出了這種迭代器支持的操作。
表10.2:插入迭代器操作
it=t | 在tt指定的當前位置插入值t。假定c是it綁定的容器,依賴于揣入迭代器的不同種類,此賦值會分別調用c.push_back(t)、c.push_front(t)或c.insert(tp),其中p為傳遞給inserter的迭代器位置 |
*it,++it,it++ | 這些操作雖然存在,但不會對it做任何事情。每個操作都返回it |
插入器有三種類型,差異在于元素插入的位置:
- back_inserter創建一個使用push_back的迭代器。
- front_inserter創建一個使用push_front的迭代器。
- inserter 創建一個使用insert的迭代器。此函數接受第二個參數,這個參數必須是一個指向給定容器的迭代器。元素將被插入到給定迭代器所表示的元素之前。
只有在容器支持push_front的情況下,我們才可以使用front_inserter。類似的,只有在容器支持push_back的情況下,我們才能使用back_inserter。
理解插入器的工作過程是很重要的:當調用inserter(c,iter)時,我們得到一個迭代器,接下來使用它時,會將元素插入到itez原秈所指向的元素之前的位置。即,如果it是由inserter生成的迭代器,則下面這樣的賦值語句
*it = val;
其效果與下面代碼一樣
it=c.insert(it,val);//it指向新加入的元素
++it;//遞增it使它指向原來的元素
front_inserter生成的迭代器的行為與insertez生成的迭代器完全不一樣。當我們使用front_inserter時,元素總是插入到容器第一個元素之前。即使我們傳遞給inserter的位置原來指向第一個元素,只要我們在此元素之前插入一個新元素,此元素就不再是容器的首元素了:
list<int>lst={1,2,3,4};
list<int>lst2,lst3;//空list
//拷貝完成之后,lst2包含4 3 2 1
copy(lst.cbegin(),lst.cend(),front_tnserter(lst2));
//拷貝完成之后,lst3包含1 2 3 4
copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));
當調用front_inserter?時,我們得到一個插入迭代器,接下來會調用push_front。當每個元素被插入到容器c中時,它變為c的新的首元素。因此,front_inserter生成的迭代器會將插入的元素序列的順序顛倒過來,而inserter和back_inserter則不會。
iostream迭代器
雖然iostream類型不是容器,但標準庫定義了可以用于這些IO類型對象的迭代器。istream_iterator讀取輸入流,ostream_iterator向一個輸出流寫數據。這些迭代器將它們對應的流當作一個特定類型的元素序列來處理。通過使用流選代器,我們可以用泛型算法從流對象讀取數據以及向其寫入數據。istream_iterator操作當創建一個流迭代器時,必須指定迭代器將要讀寫的對象類型。一個istream_iterator使用>>來讀取流。因此,istream_iterator要讀取的類型必須定義了輸入運算符。當創建一個istream_iterator時,我們可以將它綁定到一個流。當然,我們還可以默認初始化迭代器,這樣就創建了一個可以當作尾后值使用的迭代器。
istream_iterator<int>int_it(cin);//從cin讀取int
istream_iterator<int> int_eof; // 尾后迭代器
ifstream in("afile");
istream_iterator<string>str_it(in);//從“afile“讀取字符串
下面是一個用istream_iterator從標準輸入讀取數據,存入一個vector的例子:
istream_iterator<int>in_iter(cin);//從cin讀取int
istream_iterator<int>eof;//istream尾后選代器
while(in_iter!=eof)//當有數據可供讀取時
//后置途增運算讀取流,返回選代器的舊值
//解引用選代器,獲得從流讀取的前一個值
vec.push_back(*in_iter++);
此循環從cin讀取int值,保存在vec中。在每個循環步中,循環體代碼檢查in_iter是否等于eof。eof被定義為宇的istream_iterator,從而可以當作尾后迭代器來使用。對于一個綁定到流的迭代器,一于其關聯的流遇到文件尾或遇到IO錯誤,迭代器的值就與尾后迭代器相等。
此程序最困難的部分是傳遞給push_back的參數,其中用到了解引用運算符和后置遞增運算符。該表達式的計算過程與我們之前寫過的其他結合解引用和后置遞增運算的表達式一樣。后置遞增運算會從流中讀取下一個值,向前推進,但返回的是迭代器的舊值。迭代器的舊值包含了從流中讀取的前一個值,對迭代器進行解引用就能獲得此值。
我們可以將程序重寫為如下形式,這體現了istream_iterator更有用的地方:
istream_iterator<int>in_tter(cin),eof;//從cin讀取int
vector<int>vec(in_iter,eof);//從迭代器范圍構造vec
本例中我們用一對表示元素范圍的迭代器來構造vec。這兩個迭代器是istream_iterator,這意味著元素范圍是通過從關聯的流中讀取數據獲得的。這個構造函數從cin中讀取數據,直至遇到文件尾或者遇到一個不是int的數據為止。從流中讀取的數據被用來構造vec。
表10.3:istream_iterator操作
istream_iteratorin(is); | in從輸入流is讀取類型為0的值 |
istream_iteratorend; | 讀取類型為的值的istream_itterator迭代器,表示尾后位置 |
in1=in2 | in1和in2必須讀取相同類型。如果它們都是尾后迭代器,或綁定到相同 |
in1!=in2 | 的輸入,則兩者相等 |
*in | 返回從流中讀取的值 |
n->mem | 與(*in).mem的含義相同 |
++inn++使用元素類型所定義的>>運算符從輸入流中讀取下一個值。與以往一樣,前置版本返回一個指向遞增后迭代器的引用,后置版本返回舊值 |
使用算法操作流迭代器
由于算法使用迭代器操作木處理數據,而流迭代器又至少支持樹些選代器操作,因此我們至少可以用桅些算法來操作流迭代器。下面是一個例子,我們可以用一對istream_iterator來調用accumulate:
istream_iterator<int>in(cin),eof;
cout<<accumulate(in,eof,0)<<endl;
此調用會計算出從標準輸入讀取的值的和。如果輸入為:
23 109 45 89 6 34 12 90 34 23 56 23 8 89 23
則輸出為664。
istream_iterator允許使用懶惰求值
當我們將一個istream_iterator綁定到一個流時,標準庫并不保證迭代器立即從流讀取數據。具體實現可以推遲從流中讀取數據,直到我們使用迭代器時才真正讀取。標準庫中的實現所保證的是,在我們第一次解引用迭代器之前,從流中讀取數據的操作已經完成了。對于大多數程序來說,立即讀取還是推遲讀取沒什么差別。但是,如果我們創建了一個istream_iterator,沒有使用就銷毀了,或者我們正在從兩個不同的對象同步讀取同一個流,那么何時讀取可能就很重要了。
ostream_iterator操作
我們可以對任何具有輸出運算符(<<運算符的類型定義ostream_iterator。當創建一個ostream_iterator時,我們可以提供(可選的)第二參數,它是一個字符流,在輸出每個元素后都會打印此字符串。此字符串必須是一個C風格字符串(即,一個字符串字面常量或者一個指向以空字符結尾的字符數組的指針)。必須將ostream_iterator綁定到一個指定的流,不允許宇的或表示尾后位置的ostream_iterator。
表10.4:ostream_iterator操作
ostream_iterator out(os) | out將類型T的值寫到輸出流os中 |
ostream_iterator_out(os,d); | out將類型為7的值寫到輸出流os中,每個值后面都輸出一個d。d指向一個空字符結尾的字符數組 |
out= val | 用<<運算法val寫入到out所綁定的ostream中。val的類型必須與out可寫的類型兼容 |
*out,++out,out++ | 這些運算符是存在的,但不對out做任何事情。每個運算符都返回out |
我們可以用ostream_iterator來輸出值的序列:
ostream_iterator<int> out_iter(cout, " ");
for(auto e:vec)
*out_iter++ = e;//賦值語句實際上將元素寫到cout
cout<<endl;
此程序將vec中的每個元素寫到cout,每個元素后加一個空格。每次向out_iter賦值時,寫操作就會被提交。值得注意的是,當我們向out_iter賦值時,可以忽略解引用和遞增運算。即,循環可以重寫成下面的樣子:
for(auto e:vec)out_iter = e;//賦值語句將元素寫到cout
cout<<endl;
運算符*和++實際上對ostream_iterator對象不做任何事情,因此忽略它們對我們的程序沒有任何影響。但是,推薦第一種形式。在這種寫法中,流迭代器的使用與其他迭代器的使用保持一致。如果想將此循環改為操作其他迭代器類型,修改起來非常容易。而且,對于讀者來說,此循環的行為也更為清晰。可以通過調用copy來打印vec中的元素,這比編寫循環更為簡單:
copy(vec.begin(),vec.end(),out_iter);
cout<<endl;
使用流迭代器處理類類型
我們可以為任何定義了輸入運算符(>>的類型創建istream_iterator對象。類似的,只要類型有輸出運算符(C<<),我們就可以為其定義ostream_iterator。由于Sales_item既有輸入運算符也有輸出運算符:
istream_iterator<Sales_item>item_iter(cin),eof;
ostream_iterator<Sales_item>out_iter(cout,"\n");
//將第一筆交易記錄存在sum中,并讀取下一條記錄
Sales_item sum =*item_iter++;
while(item_iter!=eof){//如果當前交易記錄(存在ttem_iter中)有著相同的ISBN號if(ttem_iter->isbn()==sum.isbn())sum+=*item_iter++;//將其加到sum上并讀取下一條記錄else{out_iter= sum;//輸出sum當前值sum = *item_iter++;//讀取下一條記錄}
}
out_iter = sum;//記得打印最后一組記錄的和
此程序使用item_iter從cin讀取Sales_item交易記錄,并將和寫入cout,每個結果后面都跟一個換行符。定義了自己的迭代器后,我們就可以用item_iter讀取第一條交易記錄,用它的值來初始化sum:
//將第一條交易記錄保存在sum中,并讀取下一條記錄
Sales_item sum = *item_iter++;
此處,我們對item_iter執行后置遞增操作,對結果進行解引用操作。這個表達式讀取下一條交易記錄,并用之前保存在item_iter中的值來初始化sum。
while循環會反復執行,直至在cin上遇到文件尾為止。在while循環體中,我們檢查sum與剛剛讀入的記錄是否對應同一本書。如果兩者的ISBN不同,我們將sum賦予out_iter,這將會打印sum的當前值,并接著打印一個換行符。在打印了前一本書的交易金額之和后,我們將最近讀入的交易記錄的副本賦予sum,并遞增迭代器,這將讀取下一條交易記錄。循環會這樣持續下去,直至遇到錯誤或文件尾。在退出之前,記住要打印輸入中最后一本書的交易金額之和。
反向迭代器
反向追代器就是在容器中從尾元素向首元素反向移動的迭代器。對于反向迭代器,遞增〔以及遞減)操作的含義會顛倒過來。遞增一個反向迭代器(++it會移動到前一個元素;遞減一個迭代器(一it會移動到下一個元素。除了forward_1ist之外,其他容器都支持反向迭代器。我們可以通過調用rbegin、rend、crbegin和crend成員函數來獲得反向追代器。這些成員函數返回指向容器尾元素和首元素之前一個位置的迭代器。與普通迭代器一樣,反向迭代器也有const和非const版本。
下面的循環是一個使用反向迭代器的例子,它按逆序打印vec中的元素:
vector<int>vec={0,1,2,3,415,6,7,8,9};
//從尾元素到首元素的反向選代器
for(auto x_iter=vec.crbegin();//將_iter綁定到尾元素x_iter!=vec.crend();//crend指向首元素之前的位置++r_iter)//實際是遞減,移動到前一個元素
cout<<*r_iter<<endl;// 打印9,8,7,..。0
雖然顛倒遞增和遞減運算符的含義可能看起來令人混淆,但這樣做使我們可以用算法透明地向前或向后處理容器。例如,可以通過向sort傳遞一對反向迭代器來將vector整理為遞減序:
sort(vec.begin(),vec.end());//按“正常序“排序vec
//按遞序排序:將最小元素放在vec的末尾
sort(vec.rbegin(),vec.rend());
反向迭代器需要遞減運算符
不必驚訝,我們只能從既支持++也支持一的迭代器來定義反向迭代器。畢竟反向迭代器的目的是在序列中反向移動。除了forward_list之外,標準容器上的其他迭代器都既支持遞增運算又支持遞減運算。但是,流迭代器不支持遞減運算,因為不可能在一個流中反向移動。因此,不可能從一個forward_list或一個流迭代器創建反向迭代器。
反向迭代器和其他迭代器間的關系
假定有一個名為line的string,保存著一個逗號分隔的單詞列表,我們希望打印line中的第一個單詞。使用find可以很容易地完成這一任務:
//在一個迎號分隔的列表中查找第一個元素
auto comma=find(line.cbegin(),line.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;
如果line中有逗號,那么comma將指向這個逗號;否則,它將等于line.cend()。當我們打印從line.cbegin()到comma之間的內容時,將打印到逗號為止的字符,或者打印整個string〔如果其中不含逗號的話)。
如果希望打印最后一個單詞,可以改用反向迭代器:
// 在一個逗號分隔的列表中查找最后一個元素
auto comma=find(line.crbegin(),line.czend(),',');
由于我們將crbegin()和crend()傳遞給find,find將從1ine的最后一個字符開始向前搜索。當find完成后,如果1ine中有逗號,則rcomma指向最后一個逗號一一即,它指向反向搜索中找到的第一個逗號。如果1ine中沒有逗號,則rcomma指向
line.crend()。
當我們試圖打印找到的單詞時,最有意思的部分就來了。看起來下面的代碼是顯然的方法
//錯誤:將送序輸出單詞的字符
cout<<string(line.crbegin(),rcomma)<<endl;
但它會生成錯誤的輸出結果。例如,如果我們的輸入是
FIRST,MIDDLE,LAST
則這條語句會打印TSAL!
從技術上講,普通迭代器與反向迭代器的關系反映了左閉合區間的特性。關鍵點在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范圍。為了實現這一點,rcomma和rcomma.base()必須生成相鄰位置而不是相同位置,crbegin()和cend()也是如此。
反向迭代器的目的是表示元素范圍,而這些范圍是部隊稱的,這導致一個重要的結果:當我們從一個普通速代器初始化一個反向追代器,或是給一個反向迭代器賦值時,結果迭代器與原迭代器指向的并不是相同的元素。