C++ 迭代器總結
迭代器的重要作用就是讓容器和演算法解耦合, 或者說讓資料和操作解耦合, 演算法利用迭代器作為輸入, 從而擺脫對容器具體資料的訪問. 容器生成的迭代器用於遍歷容器中的每個元素, 同時避免暴露容器的內部資料結構和實現細節.
類似容器,迭代器也定義了一組公共操作。一些操作所有迭代器都支援,另外一些只有特定類別的迭代器才支援。比如ostream_iterator只支援遞增、解引用和賦值。而vector、string和deque的迭代器除了支援這些操作外,還支援遞減、關係和算術運算。
一:迭代器分類
迭代器可以按它們所提供的操作來分類:輸入迭代器(只讀不寫;單遍掃描,只能遞增)、輸出迭代器(只寫不讀;單遍掃描,只能遞增)、前向迭代器(可讀寫;多遍掃描,只能遞增)、雙向迭代器(可讀寫;多遍掃描,可遞增遞減)、隨機訪問迭代器(可讀寫,多遍掃描,支援全部迭代器運算)。比如,指標滿足隨機訪問迭代器所要求的所有操作,因此指標可以看做為一種隨機訪問迭代器。這種分類形成了一種層次。除了輸出迭代器之外,一個高層類別的迭代器支援低層類別迭代器的所有操作。
C++標準指明瞭泛型和數值演算法的每個迭代器引數的最小類別。例如,find演算法在一個序列上進行一遍掃描,對元素進行只讀操作,因此至少需要輸入迭代器;replace_copy的前兩個迭代器引數要求至少是前向迭代器,第三個迭代器表示目的位置,必須至少是輸出迭代器。
1:輸入迭代器(input iterator)
輸入迭代器可以讀取序列中的元素。一個輸入迭代器必須支援:
用於比較兩個迭代器的相等和不相等運算子(==、!=);
用於推進迭代器的前置和後置遞增運算(++);
用於讀取元素的解引用運算子(*),解引用只會出現在賦值運算子的右側;
箭頭運算子(->),等價於(*it).member,即解引用迭代器,井提取物件的成員;
輸入迭代器只用於順序訪問。對於一個輸入迭代器,*it++保證是有效的,輸入迭代器只能順序使用;一旦輸入迭代器自增了,就無法再用它檢查之前的元素,不能保證輸入迭代器的狀態可以儲存下來並用來訪問元素。因此,輸入迭代器只能用於單遍掃描演算法。演算法find和accumulate要求輸入迭代器。
istream_iterator是一種輸入迭代器。它使用operator>>從一種輸入流std::basic_istream物件中讀取元素。實際的讀操作是在迭代器遞增時就執行了,而不是在解引用時;而第一個物件的讀取是在迭代器構造是就進行了。解引用操作僅僅是返回讀取物件的副本。
預設構造的istream_iterator表示end-of-stream,當istream_iterator到達底層輸入流的末尾時,它就等於end-of-stream了。
當讀取字元時,istream_iterator預設行為是跳過空白符(除非使用std::noskipws),而std::istreambuf_iterator沒有這樣的行為,而且std::istreambuf_iterator更具效率,因為它避免了為每個字元建立和析構sentry物件的過程。
istream_iterator<int> eos; istream_iterator<int> is_it(cin); cout << "after is_it ctor\n"; for (; is_it != eos;) { cout << "input: " << *is_it++ << endl; } cout << "end input\n"; cin.clear(); //https://stackoverflow.com/questions/7413247/cin-clear-doesnt-reset-cin-object cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); vector<int> vec; copy(istream_iterator<int>(cin), eos, back_inserter(vec)); cout << "vec is: "; for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); ++iter) { cout << *iter << " "; } cout << endl; std::istringstream ss("0 2 3 4"); vec.clear(); copy(istream_iterator<int>(ss), eos, back_inserter(vec)); cout << "vec is: "; for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); ++iter) { cout << *iter << " "; } cout << endl;
結果如下:
1 after is_it ctor 2 input: 1 3 input: 2 4 input: 3 5 input: 4 a //無效字元 input: 5 end input 1 2 3 4 5 6 7 a vec is: 1 2 3 4 5 6 7 vec is: 0 2 3 4
這裡需要注意的是,輸入非int字串後,cin變為錯誤狀態,但是無效字元還保留在流中,因此需要呼叫cin.ignore將無效內容排出。
2:輸出迭代器(output iterator)
輸出迭代器可以看作輸入迭代器功能上的補集。它只寫而不讀元素,將元素寫入序列中。輸出迭代器必須支援:
用於推進迭代器的前置和後置遞增運算(++);
解引用運算子(*),只出現在賦值運算子的左側(向一個解引用的輸出迭代器 賦值,就是將值寫入它所指向的元素);
只能向一個輸出迭代器賦值一次。類似輸入迭代器,輸出迭代器只能用於單遍掃描算 法。用作目的位置的迭代器通常都是輸出迭代器。例如,copy函式的第三個引數就是輸出迭代器。
ostream_iterator型別也是輸出迭代器。它使用operator<<將元素寫入到輸出流中,構造ostream_iterator時,除了指定輸出流之外,還可以指定一個可選的分隔符,這樣每次operator<<輸出一個元素之後,還會輸出該分隔符。注意,當寫字元時,使用std::ostreambuf_iterator效率更高,因為它避免了為每個字元構造和析構sentry物件的開銷。
vector<int> vi; ostream_iterator<int> os_it(cout, ","); for (int i = 0; i < 10; i++) { vi.push_back(i); } for(vector<int>::iterator iter = vi.begin(); iter != vi.end(); ++iter) { *os_it++ = *iter; } cout << endl; copy(vi.begin(), vi.end(), os_it); cout << endl; ostringstream ss; ostream_iterator<string> oss(ss, "|"); copy(istream_iterator<string>(cin), istream_iterator<string>(), oss); cout << ss.str() << endl;
實際上,對於ostream_iterator而言,上面的*os_it++ = *iter;寫成os_it = *iter也是可以的,這是因為The write operation is performed when the iterator (whether dereferenced or not) is assigned to. Incrementing the std::ostream_iterator is a no-op。因此ostream_iterator的operator*和operator++什麼也不做,為它定義這兩個操作符僅僅是因為These operator overloads are provided to satisfy the requirements of LegacyOutputIterator。
一般*os_it++ = *iter這樣的寫法不常見,而是使用copy演算法。
3:前向迭代器(forward iterator)
前向迭代器可以讀寫元素。這類迭代器只能在序列中沿一個方向移動。前向迭代器支援所有輸入和輸出迭代器的操作,而且可以多次讀寫同一個元素。因此,可以儲存前向迭代器的狀態,使用前向迭代器的演算法可以對序列進行多遍掃描。演算法replace要求前向迭代器;容器forward_list、unordered_set、unordered_multiset、unordered_map、unordered_multimap上的迭代器是前向迭代器。
4:雙向迭代器(bidirectional iterator)
雙向迭代器可以正向或反向讀寫序列中的元素。除了支援所有前向迭代器的操作之外,雙向迭代器還支援前置和後置遞減運算子(--)。演算法reverse要求雙向迭代器,容器list、map、set 、multiset、multimap上的迭代器是雙向迭代器。
5:隨機訪問迭代器(random-access iterator)
隨機訪問迭代器提供在常量時間內訪問序列中任意元素的能力。此類迭代器支援雙向迭代器的所有功能,此外還支援iter+n; iter-n; iter+=n; iter-=n; iter1-iter2; >; >=; <; <=; 等操作。
演算法sort要求隨機訪問迭代器。array、deque、string和vector的迭代器都是隨機訪問迭代器,用於訪問內建陣列元素的指標也是。
二:其他迭代器
除了為每個容器定義的迭代器之外,標準庫在標頭檔案<iterator>中還定義了額外幾種迭代器。這些迭代器包括:
插入迭代器(insert iterator):這類迭代器被繫結到一個容器上,可用來向容器插入元素;
流迭代器(stream iterator):這些迭代器被繫結到輸入或輸出流上,可用來遍歷所關聯的IO流;
反向迭代器(reverse iterator):這些迭代器向後而不是向前移動。除了forward_list之外的標準庫容器都有反向迭代器;
移動迭代器(move iterator):這些專用的迭代器不是拷貝其中元素,而是移動它們;
1:插人迭代器
插入器是一種迭代器介面卡,它接受一個容器,生成一個插入迭代器,能實現向給定容器新增元素。當我們通過一個插入迭代器進行賦值時,該迭代器呼叫容器操作來向給定容器的指定位置插入一個元索。
it=t:在it指定的當前位置插入值t。假定c是it繫結的容器,根據插入迭代器的不同種類,賦值操作會分別呼叫c.push_back(t);c.push_front(t)或c.insert(t, p);其中p為傳遞給inserter的迭代器位置;
*it, ++it, it++這些操作雖然存在,但是不會對it做任何事情,也就是說,它們都是no-op操作,每個操作都返t;
插入器有三種類型,區別在於元素插入的位置:
template< class Container > std::back_insert_iterator<Container> back_inserter( Container& c );
back_inserter建立一個使用push_back的迭代器(容器必須支援push_back),即std::back_insert_iterator,這是一種輸出迭代器,當這種迭代器被賦值時,實際上是呼叫了容器的push_back成員函式。這種迭代器的operator*和operator++操作實際上都是一種no-op操作。
template< class Container > std::front_insert_iterator<Container> front_inserter( Container& c );
front_inserter建立一個使用push_front的迭代器(容器必須支援push_front),即std::front_insert_iterator迭代器,類似於std::back_insert_iterator,這種迭代器是一種輸出迭代器,當這種迭代器被賦值時,實際上是呼叫了容器的push_front成員函式。這種迭代器的operator*和operator++操作實際上都是一種no-op操作。
template< class Container > std::insert_iterator<Container> inserter( Container& c, typename Container::iterator i );
inserter建立一個使用insert的迭代器,即insert_iterator,這也是一種輸出迭代器,對其賦值相當於呼叫容器的insert()成員函式,這種迭代器的operator*和operator++也是no-op操作。
inserter函式的引數都用於構造std::insert_iterator,它接受第二個引數,這個引數必須是一個指向給定容器的迭代器。元素將被插入到給定迭代器所表示的元索之前。
當呼叫inserter(c, iter)時,我們得到一個迭代器,接下來使用它時,會將元索插入到iter原來所指向的元素之前的位置。即,如果it是由inserter生成的迭代器,則下面這樣的賦值語句:*it=val; 其效果與下面的程式碼一樣:
it = c.insert(it, val); //it指向新加入的元素 ++it; //遞增it使它指向原來的元素
front_inserter生成的迭代器的行為與inserter的迭代器完個不一樣。使用front_inserter時,元素總是插入到容器第一個元素之前。而即使我們傳遞給inserter的位置原來指向第一個元素,只要我們在此元素之前插入一個新元素,此元素就不再是容器的首元素了:
list<int> lst = {1,2,3,4}; list<int> lst2, lst3; //拷貝完成之後,lst2包含4 3 2 1 copy(lst.cbegin(), lst.end(), front_inserter(lst2)); //拷貝完成之後,lst3包含1 2 3 4 copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));
當呼叫front_inserter(c)時,我們得到一個插入迭代器,接下來會呼叫push_front。當每個元素被插入到容器C中時,它變為C的新的首元素。因此,front_inserter生成的迭代器會將插入的元素序列的順序顛倒過來,而inserter和back_inserter則不會。
2:反向迭代器
template< class Iter > class reverse_iterator;
std::reverse_iterator實際上是一種迭代器介面卡。對於給定的雙向迭代器,std::reverse_iterator就會產生一個新的迭代器,可以沿著底層雙向迭代器定義的反方向進行移動。如果反向迭代器r是由迭代器i建立的,則表示式&*r == &*(i-1) 永遠為真。
反向迭代器需要遞減運算子,我們只能從既支援++也支援--的雙向迭代器來定義反向迭代器。除了forward_list之外,標準容器上的其他迭代器都既支援遞增運算又支援遞減運算。但是,流迭代器不支援遞減運算,因為不可能在一個流中反向移動。
容器中的reverse_iterator實際上就是std::reverse_iterator<iterator>的別名。反向迭代器就在容器中從尾元素向首元素反向移動。對於反向迭代器,遞增以及遞減操作的含義會顛倒過來。遞增一個反向迭代器(++it)會移動到前一個元素,遞減一個迭代器(--it)會移動到下一個元素。
可以通過呼叫容器的rbegin,rend, crbegin和crend成員函式來獲得反向迭代器。這些成員函式返回指向容器尾元素和首元素之前一個位置的迭代器。與普通迭代器一樣,反向迭代器也有const和非const版本。下圖顯示了一個vector上的4種迭代器:
雖然顛倒遞增和遞減運算子的含義可能看起來令人混淆,但這樣做使我們可以用演算法透明地向前或向後處理容器。例如,可以通過向sort傳遞一對反向迭代器來將vector整理為遞減序:
sort(vec.begin(), vec.end()); //按 正常序 排序vec sort(vec.rbegin(), vec.rend()); //按逆序排序,將最小元素放在vec的末尾
假定有一個名為line 的string,儲存著一個逗號分隔的單詞列表,我們希望列印line中的第一個單詞。使用find可以很容易地完成這一仟務:
auto comma=find(line.cbegin(), line.cend(), ','); cout << string(line.cbegin(), comma) << endl;
如果希望列印最後一個單詞,可以改用反向迭代器,但是下面的程式碼是有問題的,它會逆序輸出單詞的字元:
auto rcomma = find(line.cbegin(), line.cend(), ',');. cout << string(line.crbegin(), rcomma) << endl;
我們使用的是反向迭代器,會反向處理string。因此,上述輸出語句從crbegin開始反向列印line中內容。而我們希望按正常順序列印從rcomma開始到line末尾間的字元。此時需要將rcomma轉換回一個普通迭代器,能在line中正向移動。通過呼叫reverse_iterator的base成員函式來完成這一轉換,此成員函式會返回其對應的普通迭代器:
//正確:得到一個正向迭代器,從逗號開始讀取字元直到末尾 cout << string(rcomma.base(), line.cend()) << endl:
需要注意的是,rcomma和rcomma.base()指向不同的元素,就像line.crbegin和line.cend()的關係一樣。
3:移動迭代器
template< class Iter > class move_iterator;
C++11定義了一種移動迭代器(move iterator)介面卡std::move_iterator。一個移動迭代器通過改變給定迭代器的解引用運算子的行為來適配此迭代器。一般來說,一個迭代器的解引用運算子返回一個指向元素的左值。與其他迭代器不同,移動迭代器的解引用生成一個右值引用,所以,如果移動迭代器作為輸入迭代器的話,迭代器指向的值是被移動的,而非複製。
可以呼叫標準庫的std::make_move_iterator函式將一個普通迭代器轉換為一個移動迭代器。此函式接受一個迭代器引數,返回一個移動迭代器。
原迭代器的所有其他操作在移動迭代器中都照常工作。由於移動迭代器支援正常的迭代器操作,我們可以將一對移動迭代器傳遞給演算法。
值得注意的是,標準庫不保證哪些演算法適用移動迭代器,哪些不適用。由於移動一個物件可能銷燬掉原物件,因此你只有在確信演算法在為一個元素賦值或將其傳遞給一個使用者定義的函式後不再訪問它時,才能將移動迭代器傳遞給演算法。
std::list<std::string> s{"one", "two", "three"}; std::vector<std::string> v1(s.begin(), s.end()); // copy std::vector<std::string> v2(std::make_move_iterator(s.begin()), std::make_move_iterator(s.end())); // move std::cout << "v1 now holds: "; for (auto str : v1) std::cout << "\"" << str << "\" "; std::cout << "\nv2 now holds: "; for (auto str : v2) std::cout << "\"" << str << "\" "; std::cout << "\noriginal list now holds: "; for (auto str : s) std::cout << "\"" << str << "\" "; std::cout << '\n';
結果是:
v1 now holds: "one" "two" "three" v2 now holds: "one" "two" "three" original list now holds: "" "" ""
4:std::istreambuf_iterator和std::ostreambuf_iterator
std::istreambuf_iterator行為類似於std::istream_iterator,它也是單次遍歷的輸入迭代器,只是它僅用於從std::basic_streambuf物件中讀取字元(char、wchar_t等),而且效率更高。
std::istringstream in("Hello, world"); std::vector<char> v( (std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>() ); std::cout << "v has " << v.size() << " bytes. "; v.push_back('\0'); std::cout << "it holds \"" << &v[0] << "\"\n";
結果是:
v has 12 bytes. it holds "Hello, world"
std::ostreambuf_iterator行為類似於std::istream_iterator,它也是單次遍歷的輸出迭代器,只是它僅用於輸出字元(char、wchar_t等)到std::basic_streambuf物件,而且效率更高。
std::string s = "This is an example\n"; std::copy(s.begin(), s.end(), std::ostreambuf_iterator<char>(std::cout));
結果是:
This is an example
https://www.jianshu.com/p/8a51cf91a293
https://en.cppreference.com/