【Qt筆記】遍歷容器

上一節咱們大體瞭解了有關存儲容器的相關內容。對於全部的容器,最經常使用的操做就是遍歷。本章咱們將詳細瞭解有關遍歷器的內容。算法

儘管這個問題不是本章須要考慮的,可是咱們仍是須要來解釋下,爲何要有遍歷器。沒有遍歷器時,若是咱們須要向外界提供一個列表,咱們一般會將其返回:數組

QList<int> intlist() const
{
    return list;
}

這麼作的問題是:向用戶暴露了集合的內部實現。用戶知道,原來你用的就是一個QList啊~那我就能夠向裏面增長東西了,或者修改其中的內容。有時這不是咱們所指望的。不少時候,咱們只是想提供用戶一個集合,只容許用戶知道這個集合中有什麼,而不是對它進行修改。爲此,咱們但願有這麼一種對象:經過它就可以提供一種通用的訪問集合元素的方法,無論底層的集合是鏈表仍是散列,均可以經過這種對象實現。這就是遍歷器。函數

 

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

Java 風格的遍歷器

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

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

容器 只讀遍歷器 讀寫遍歷器
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 風格的遍歷器指向的是兩個元素之間的位置,而不是指向元素自己。所以,它們可能會指向集合第一個元素以前的位置,也可能指向集合的最後一個元素以後的位置,以下圖所示:code

咱們經過下面的代碼看看如何使用這種遍歷器:對象

QList<QString> list;
list << "A" << "B" << "C" << "D";

QListIterator<QString> i(list);
while (i.hasNext()) {
    qDebug() << i.next();
}

首先,咱們使用 list 對象建立一個遍歷器。剛剛建立完成時,該遍歷器位於第一個元素以前(也就是 A 以前)。咱們經過調用hasNext()函數判斷遍歷器以後的位置上有無元素。若是有,調用next()函數將遍歷器跳過其後的元素。next()函數返回剛剛跳過的元素。固然,咱們也可使用hasPrevious()previous()函數來從尾部開始遍歷,詳細內容能夠參考 API 文檔。接口

QListIterator是隻讀遍歷器,不能插入或者刪除數據。若是須要這些操做,咱們可使用QMutableListIterator。來看下面的代碼:

QMutableListIterator<int> i(list);
while (i.hasNext()) {
    if (i.next() % 2 != 0) {
        i.remove();
    }
}

這段代碼使用QMutableListIterator遍歷集合,若是其值是奇數則將其刪除。在每次循環中都要調用next()函數。正如前面所說,它會跳過其後的一個元素。remove()函數會刪除咱們剛剛跳過的元素。調用remove()函數並不會將遍歷器置位不可用,所以咱們能夠連續調用這個函數。向前遍歷也是相似的,這裏再也不贅述。

若是咱們須要修改已經存在的元素,使用setValue()函數。例如:

QMutableListIterator<int> i(list);
while (i.hasNext()) {
    if (i.next() > 128) {
        i.setValue(128);
    }
}

如同remove()函數,setValue()也是對剛剛跳過的元素進行操做。實際上,next()函數返回的是集合元素的非 const 引用,所以咱們根本不須要調用setValue()函數:

QMutableListIterator<int> i(list);
while (i.hasNext()) {
    i.next() *= 2;
}

QMapItrator也是相似的。例如,使用QMapItrator咱們能夠將數據從QMap複製到QHash

QMap<int, QWidget *> map;
QHash<int, QWidget *> hash;

QMapIterator<int, QWidget *> i(map);
while (i.hasNext()) {
    i.next();
    hash.insert(i.key(), i.value());
}

STL 風格的遍歷器

STL 風格的遍歷器從 Qt 2.0 就開始提供。這種遍歷器可以兼容 Qt 和 STL 的通用算法,而且爲速度進行了優化。同 Java 風格遍歷器相似,Qt 也提供了兩種 STL 風格的遍歷器:一種是隻讀訪問,一種是讀寫訪問。咱們推薦儘量使用只讀訪問,由於它們要比讀寫訪問的遍歷器快一些。

容器 只讀遍歷器 讀寫遍歷器
QList<T>,QQueue<T> QList<T>::const_iterator QList<T>::iterator
QLinkedList<T> QLinkedList<T>::const_iterator QLinkedList<T>::iterator
QVector<T>,QStack<T> QVector<T>::const_iterator QVector<T>::iterator
QSet<T> QSet<T>::const_iterator QSet<T>::iterator
QMap<Key, T>,QMultiMap<Key, T> QMap<Key, T>::const_iterator QMap<Key, T>::iterator
QHash<Key, T>,QMultiHash<Key, T> QHash<Key, T>::const_iterator QHash<Key, T>::iterator

STL 風格的遍歷器具備相似數組指針的行爲。例如,咱們可使用 ++ 運算符讓遍歷器移動到下一個元素,使用 * 運算符獲取遍歷器所指的元素。對於QVectorQStack,雖然它們是在連續內存區存儲元素,遍歷器類型是typedef T *const_iterator類型則是typedef const T *

咱們仍是以QListQMap爲例,理由如上。下面是有關QList的相關代碼:

QList<QString> list;
list << "A" << "B" << "C" << "D";

QList<QString>::iterator i;
for (i = list.begin(); i != list.end(); ++i) {
    *i = (*i).toLower();
}

不一樣於 Java 風格遍歷器,STL 風格遍歷器直接指向元素自己。容器的begin()函數返回指向該容器第一個元素的遍歷器;end()函數返回指向該容器最後一個元素以後的元素的遍歷器。end()實際是一個非法位置,永遠不可達。這是爲跳出循環作的一個虛元素。若是集合是空的,begin()等於end(),咱們就不能執行循環。

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

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

QList<QString>::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i) {
    qDebug() << *i;
}

QMapQHash的遍歷器,* 運算符返回集合鍵值對。下面的代碼,咱們打印出QMap的全部元素:

QMap<int, int> map;

QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
    qDebug() << i.key() << ":" << i.value();
}

因爲有隱式數據共享(咱們會在後面的章節介紹該部份內容),即便一個函數返回集合中元素的值也不會有很大的代價。Qt API 包含了不少以值的形式返回QListQStringList的函數(例如QSplitter::sizes())。若是你但願使用 STL 風格的遍歷器遍歷這樣的元素,應該使用遍歷器遍歷容器的拷貝,例如:

// 正確的方式
const QList<QString> sizes = splitter->sizes();
QList<QString>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
    ...

// 錯誤的方式
QList<QString>::const_iterator i;
for (i = splitter->sizes().begin();
     i != splitter->sizes().end(); ++i)
    ...

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

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

QVector<int> a, b;
a.resize(100000); // 使用 0 填充一個很是大的 vector

QVector<int>::iterator i = a.begin();
// 使用遍歷器 i 的錯誤方式(注意,此時,a 上面已經有一個正在遍歷的遍歷器):
b = a;
/*
    如今,咱們的萬分當心遍歷器 i。由於它指向了共享的數據。
    若是咱們執行語句 *i = 4,咱們就會改變了共享的數據實例(兩個 vector 都會被改變)。
    這裏的行爲與 STL 容器不一樣,所以這種問題僅出如今 Qt 中;使用 STL 標準容器不存在這個問題。
*/

a[0] = 5;
/*
    如今,容器 a 被修改了,其實際數據已經與共享數據不一樣,
    即便 i 就是從容器 a 建立的遍歷器,可是它指向的數據與 a 並不一致,其表現就像是 b 的遍歷器。
    這裏的情形是:(*i) == 0.
*/

b.clear(); // 如今咱們清空 b,此時,遍歷器 i 已經不可用了。

int j = *i; // 無定義行爲!
/*
    來自 b 的數據(也就是 i 指向的那些數據)已經被銷燬了。
    這種行爲在 STL 容器中是徹底可行的(在 STL 容器中,(*i) == 5),
    可是使用 QVector 則頗有可能出現崩潰。
*/

雖然這個例子只演示了QVector,但實際上,這個問題適用於全部隱式數據共享的容器類。

foreach關鍵字

若是咱們僅僅想要遍歷集合全部元素,咱們可使用 Qt 的foreach關鍵字。這個關鍵字是 Qt 特有的,經過預處理器進行處理。C++ 11 也提供了本身的foreach關鍵字,不過與此仍是有區別的。

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

QLinkedList<QString> list;
...
QString str;
foreach (str, list) {
    qDebug() << str;
}

這段代碼與下面是等價的:

QLinkedList<QString> list;
...
QLinkedListIterator<QString> i(list);
while (i.hasNext()) {
    qDebug() << i.next();
}

若是類型名中帶有逗號,好比QPair<int, int>,咱們只能像上面同樣,先建立一個對象,而後使用foreach關鍵字。若是沒有逗號,則能夠直接在foreach關鍵字中使用新的對象,例如:

QLinkedList<QString> list;
...
foreach (const QString &str, list) {
    qDebug() << str;
}

Qt 會在foreach循環時自動拷貝容器。這意味着,若是在遍歷時修改集合,對於正在進行的遍歷是沒有影響的。即便不修改容器,拷貝也是會發生的。可是因爲存在隱式數據共享,這種拷貝仍是很是迅速的。

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

相關文章
相關標籤/搜索