1. 程式人生 > >C++11 你真的會用迭代器(iterator)麼?

C++11 你真的會用迭代器(iterator)麼?

C++ STL提供了豐富的標準容器(Container)物件(vector,array,queue,list,set,unordered_map/set…),讓我們可以根據需求選擇不同的容器管理各種型別的資料。說到使用容器,不用迭代器(iterator)是不可能的,所有的容器物件都根據容器的特點都提供了類似但不同的iterator,用於訪問容器中的資料。

迭代器(iterator)迴圈

一般來說,如果要遍歷一個容器中的所有資料,程式設計師們最常用的寫法是:

#include <list>
#include <iostream>
int main(){
list
<int>
lst; for(list<int>::iterator itor=lst.begin();itor!=lst.end();itor++){ cout<<(*itor)<<endl; //do something } }

基於範圍的for迴圈

C++11提供了關於for迴圈的新特性:基於範圍的for迴圈( the range-base for statement),再加上”型別推導”特性可以將上面的程式碼進一步簡化:

for(auto &node:lst){
    cout<<node<<endl;
    //do something
}

沒有區別嗎?

顯然,新的for迴圈寫法更簡潔,但新的for迴圈寫法的優點僅此而已嗎?
仔細琢磨,你會注意到,第一種寫法,每次迴圈,都要呼叫lst.end(),

這是list.end()函式的原始碼(來自C++11中標頭檔案stl_list.h):

      /**
       *  Returns a read/write iterator that points one past the last
       *  element in the %list.  Iteration is done in ordinary element
       *  order.
       */
iterator end() _GLIBCXX_NOEXCEPT { return iterator(&this->_M_impl._M_node); }

可以看出,每一次呼叫end()函式,都會返回一個iterator物件,根據迭代器的特性我們可以知道在整個迭代迴圈過程中,每次呼叫end()返回的物件其實都是完全一樣的,而每次的呼叫都不可避免會發生物件構造、複製。。。等等動作,這對於要求高效能的應用場合,這種無意義的重複是不可接受的。

那麼基於範圍的for迴圈( the range-base for statement)會不會是同樣的處理方式呢?
為了驗證這個問題,我做了一個試驗:
在我的上一篇文章
《C++11 為自定義容器實現標準的forward迭代器》中我實現了一個基於自定義雜湊表(HashTableAbstract)的標準forward迭代器。於是我在HashTableAbstract 的end()函式中加入了除錯程式碼,這樣每次end()都會輸出除錯資訊:

    iterator end()noexcept
    //{ return iterator(this->m_table,this->m_table.capacity); }//原始碼
    {
        cout << "return a end iterator" << endl;//輸出除錯資訊
        return iterator(this->m_table, this->m_table.capacity);
    }

然後執行如下測試程式碼:

    HashSetCl<int> testhash;//HashSetCl是基於HashTableAbstract子類,實現雜湊集合
    testhash.insert(2000);//加入3條資料
    testhash.insert(65535);
    testhash.insert(12345);
    cout<<"testing for statement using iterator:"<<endl;//使用迭代器迴圈
    for (auto itor = testhash.begin(); itor != testhash.end(); itor++) {
        cout << *itor << endl;
    }
    cout<<"testing for the range-base for statement:"<<endl;//使用基於範圍的for迴圈
    for (auto n : testhash) {
        cout << n << endl;
    }

以下是程式輸出(debug/release結果一樣)

testing for statement using iterator://注,迴圈中呼叫了三次end()
return a end iterator
2000
return a end iterator
12345
return a end iterator
65535
return a end iterator
testing for the range-base for statement://注,迴圈中呼叫了一次end()
return a end iterator
2000
12345
65535

總結

上面的輸出可以證實,基於範圍的for迴圈( the range-base for statement)只會在迴圈開始的時候呼叫一次end(),與一般直接使用迭代器(iterator)的迴圈相比,不僅具備程式碼更簡潔的優點,效能上也更具備優勢。當然這個結論只在無序容器迭代遍歷(只讀)的情況下才有效(無序容器只提供forward迭代器),具備隨機訪問迭代器(random-access iterator)的容器(比如 vector,array),直接用下標訪問才是最快的。

如果你還是”堅持傳統”,習慣直接使用迭代器來工作,那麼建議對程式碼做一些改進,還以最前面的程式碼為例,在迴圈開始時呼叫一次end()函式儲存成臨時變數end,然後每次迴圈比較的時候不再呼叫end()函式,而是直接與臨時變數end相比,就避免了重複呼叫end()

for(auto itor=lst.begin(),end=lst.end();itor!=end;itor++){
    cout<<(*itor)<<endl;
    //do something
}