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
}