QT5容器遍歷

huffscan發表於2017-09-25

Qt 的容器類提供了兩種風格的遍歷器:Java 風格和 STL 風格。這兩種風格的遍歷器在通過非 const 函式對集合進行修改時都是不可用的。

Java 風格的遍歷器

Java 風格的遍歷器是在 Qt4 首先引入的,是 Qt 應用程式首先推薦使用的形式。這種風格比起 STL 風格的遍歷器更方便。方便的代價就是不如後者高效。它們的 API 非常類似於 Java 的遍歷器類,故名。

每一種容器都有兩種 Java 風格的遍歷器:一種提供只讀訪問,一種提供讀寫訪問:

容器 只讀遍歷器 讀寫遍歷器
QList<T>,QQueue<T> QListIterator<T> QMutableListIterator<T>
QLinkedList<T> QLinkedListIterator<T> QMutableLinkedListIterator<T>
QVector<T>,QStack<T> QVectorIterator<T> QMutableVectorIterator<T>
QSet<T> QSetIterator<T> QMutableSetIterator<T>
QMap<Key, T>,QMultiMap<Key, T> QMapIterator<T> QMutableMapIterator<T>
QHash<Key, T>,QMultiHash<Key, T> QHashIterator<T> QMutableHashIterator<T>

這裡我們只討論QListQMap的遍歷器。QLinkedListQVectorQSet的遍歷器介面與QList的是一樣的;QHash遍歷器的介面則同QMap是一樣的。

不同於下面我們將要介紹的 STL 風格的遍歷器,Java 風格的遍歷器指向的是兩個元素之間的位置,而不是指向元素本身。因此,它們可能會指向集合第一個元素之前的位置,也可能指向集合的最後一個元素之後的位置,如下圖所示:

Java Style Iterator

我們通過下面的程式碼看看如何使用這種遍歷器:

首先,我們使用 list 物件建立一個遍歷器。剛剛建立完成時,該遍歷器位於第一個元素之前(也就是 A 之前)。我們通過呼叫hasNext()函式判斷遍歷器之後的位置上有無元素。如果有,呼叫next()函式將遍歷器跳過其後的元素。next()函式返回剛剛跳過的元素。當然,我們也可以使用hasPrevious()previous()函式來從尾部開始遍歷,詳細內容可以參考 API 文件。

QListIterator是隻讀遍歷器,不能插入或者刪除資料。如果需要這些操作,我們可以使用QMutableListIterator。來看下面的程式碼:

這段程式碼使用QMutableListIterator遍歷集合,如果其值是奇數則將其刪除。在每次迴圈中都要呼叫next()函式。正如前面所說,它會跳過其後的一個元素。remove()函式會刪除我們剛剛跳過的元素。呼叫remove()函式並不會將遍歷器置位不可用,因此我們可以連續呼叫這個函式。向前遍歷也是類似的,這裡不再贅述。

如果我們需要修改已經存在的元素,使用setValue()函式。例如:

如同remove()函式,setValue()也是對剛剛跳過的元素進行操作。實際上,next()函式返回的是集合元素的非 const 引用,因此我們根本不需要呼叫setValue()函式:

QMapItrator也是類似的。例如,使用QMapItrator我們可以將資料從QMap複製到QHash

STL 風格的遍歷器

STL 風格的遍歷器從 Qt 2.0 就開始提供。這種遍歷器能夠相容 Qt 和 STL 的通用演算法,並且為速度進行了優化。同 Java 風格遍歷器類似,Qt 也提供了兩種 STL 風格的遍歷器:一種是隻讀訪問,一種是讀寫訪問。我們推薦儘可能使用只讀訪問,因為它們要比讀寫訪問的遍歷器快一些。

容器只讀遍歷器讀寫遍歷器
QList<T>,QQueue<T>QList<T>::const_iteratorQList<T>::iterator
QLinkedList<T>QLinkedList<T>::const_iteratorQLinkedList<T>::iterator
QVector<T>,QStack<T>QVector<T>::const_iteratorQVector<T>::iterator
QSet<T>QSet<T>::const_iteratorQSet<T>::iterator
QMap<Key, T>,QMultiMap<Key, T>QMap<Key, T>::const_iteratorQMap<Key, T>::iterator
QHash<Key, T>,QMultiHash<Key, T>QHash<Key, T>::const_iteratorQHash<Key, T>::iterator

STL 風格的遍歷器具有類似陣列指標的行為。例如,我們可以使用 ++ 運算子讓遍歷器移動到下一個元素,使用 * 運算子獲取遍歷器所指的元素。對於QVectorQStack,雖然它們是在連續記憶體區儲存元素,遍歷器型別是typedef T *const_iterator型別則是typedef const T *

我們還是以QListQMap為例,理由如上。下面是有關QList的相關程式碼:

不同於 Java 風格遍歷器,STL 風格遍歷器直接指向元素本身。容器的begin()函式返回指向該容器第一個元素的遍歷器;end()函式返回指向該容器最後一個元素之後的元素的遍歷器。end()實際是一個非法位置,永遠不可達。這是為跳出迴圈做的一個虛元素。如果集合是空的,begin()等於end(),我們就不能執行迴圈。

下圖是 STL 風格遍歷器的示意圖:

STL 風格遍歷器

我們使用const_iterator進行只讀訪問,例如:

QMapQHash的遍歷器,* 運算子返回集合鍵值對。下面的程式碼,我們列印出QMap的所有元素:

由於有隱式資料共享(我們會在後面的章節介紹該部分內容),即使一個函式返回集合中元素的值也不會有很大的代價。Qt API 包含了很多以值的形式返回QListQStringList的函式(例如QSplitter::sizes())。如果你希望使用 STL 風格的遍歷器遍歷這樣的元素,應該使用遍歷器遍歷容器的拷貝,例如:

對於那些返回集合的 const 或非 const 引用的函式,就不存在這個問題。

另外,隱式資料共享對 STL 風格遍歷器造成的另一個影響是,當一個容器正在被一個遍歷器遍歷的時候,不能對這個容器進行拷貝。如果你必須對其進行拷貝,那麼就得萬分小心。例如,

雖然這個例子只演示了QVector,但實際上,這個問題適用於所有隱式資料共享的容器類。

foreach關鍵字

如果我們僅僅想要遍歷集合所有元素,我們可以使用 Qt 的foreach關鍵字。這個關鍵字是 Qt 特有的,通過前處理器進行處理。C++ 11 也提供了自己的foreach關鍵字,不過與此還是有區別的。

foreach的語法是foreach (variable, container)。例如,我們使用foreachQLinkedList進行遍歷:

這段程式碼與下面是等價的:

如果型別名中帶有逗號,比如QPair<int, int>,我們只能像上面一樣,先建立一個物件,然後使用foreach關鍵字。如果沒有逗號,則可以直接在foreach關鍵字中使用新的物件,例如:

Qt 會在foreach迴圈時自動拷貝容器。這意味著,如果在遍歷時修改集合,對於正在進行的遍歷是沒有影響的。即使不修改容器,拷貝也是會發生的。但是由於存在隱式資料共享,這種拷貝還是非常迅速的。

因為foreach建立了集合的拷貝,使用集合的非 const 引用也不能實際修改原始集合,所修改的只是這個拷貝。

相關文章