Qt——容器類(譯)


注:本文是我對Qt官方文檔的翻譯,錯誤之處還請指正。

原文鏈接:Container Classes

 

介紹

Qt庫提供了一套通用的基於模板的容器類,可以用這些類存儲指定類型的項。比如,你需要一個大小可變的QString的數組,則使用QVector<QString>。

這些容器類比STL(C++標准模板庫)容器設計得更輕量、更安全並且更易於使用。如果對STL不熟悉,或者傾向於用“Qt的方式”,那么你可以使用這些類,而不去用STL的類。

這些容器類是隱式共享的(可參考我的一篇博文)、可重入的,並且對速度、內存消耗等進行了優化。除此之外,當它們作為只讀的容器時是線程安全的,所有線程都可以使用它們。

你可以用兩種方式遍歷容器內存儲的項:Java風格的迭代器和STL風格的迭代器。Java風格的迭代器更易於使用,並且提供了更高級的功能;STL風格的迭代器更高效,並且可以和Qt與STL的泛型算法一起使用。

Qt還提供了foreach關鍵字使我們方便地遍歷容器中的項。

 

容器類

Qt提供了幾個有序容器:QList、QLinkedList、QVector、QStack和QQueue。大多數時候,QList是最好的選擇,雖然是用數組實現的,但在它的首尾添加元素都非常快。如果你需要一個鏈表(linked-list)就用QLinkedList;想要你的項在內存中連續存儲,就使用QVector。QStack和QQueue(棧和隊列)分別提供了后進先出(LIFO)和先進先出(FIFO)的機制。

Qt還有一些關聯容器:QMap、QMultiMap、QHash、QMultiHash、QSet。“Multi”容器支持一個鍵對應多個值。“Hash”容器在有序集合上使用hash函數進行快速的查找,而沒有用二叉搜索。

作為特殊的情況,QCache和QContiguousCache類在有限的緩存中提供對對象高效的哈希查找。

概述
QList<T> 這是目前使用最頻繁的容器類,它存儲了指定類型(T)的一串值,可以通過索引來獲得。本質上QList是用數組實現的,從而保證基於索引的訪問非常快。可以通過QList::append()和QList::prepend在兩端添加項,或者通過QList::insert()在中間插入項。QStringList是從QList<QString>得到的。
QLinkedList<T>  類似於QList,但它使用迭代器而不是整數索引來獲得項。當在一個很大的list中間插入項時,它提供了更好的性能,並且它有更好的迭代器機制。(只要那一項存在,指向那一項的迭代器依然保持有效。但插入或移除之后,QList中的迭代器可能會失效)
QVector<T>  在內存中相鄰的位置存儲一組值,在開頭或中間插入會非常慢,因為它會導致內存中很多項移動一個位置。
QStack<T>  QVector的一個子類,提供后進先出的機制。在當前的QVector中增加了幾個方法:push()、pos()、top()。
QQueue<T>  QList的一個子類,提供了先進先出的機制,在當前的QList中增加了幾個方法:enqueue()、dequeue()、head()。
QSet<T>  單值的數學集合,能夠快速查找。
QMap<Key, T>  提供了字典(關聯數組)將類型Key的鍵對應類型T的值。通常一個鍵對應一個值,QMap以Key的順序存儲數據,如果順序不重要,QHash是一個更快的選擇。
QMultiMap<Key, T>  QMap的子類,提供了多值的接口,一個鍵對應多個值。
QHash<Key, T>  和QMap幾乎有着相同的接口,但查找起來更快。QHash存儲數據沒有什么順序。
QMultiHash<Key, T>  QHash的子類,提供了多值的接口。

 

容器是可嵌套的。比如當鍵是QString類型、值是QList<int>類型時,用QMap<QString, QList<int> >是最好的選擇,唯一的缺點是你必須在結尾的兩個尖括號(>)之間插入一個空格,否則C++編譯器可能會誤將兩個>當作右移運算符來解釋,出現語法錯誤。

存儲在容器中的值可以是任何可賦值的數據類型,為了達到這一點,一個類型必須有默認構造函數、拷貝構造函數還有一個賦值運算符。這個已經涵蓋了大多數你可能想要存在容器中的類型,包括基本類型,比如int和double、指針類型,還有Qt中的類型,比如QString、QDate和QTime,但是它不包括QObject或者QObject的子類(QWidget, QDialog, QTimer等等)。如果你嘗試使用QList<QWidget>,編譯器可能會提示QWidget的拷貝構造函數和賦值操作符不可用。所以如果你想在容器中存儲這些類型,把它們當做指針就行了,比如QList<QWidget *>。

這里有一個例子,達到可賦值的數據類型條件的一個普通數據類型:

class Employee
{
public:
    Employee() {}
    Employee(const Employee &other);

    Employee &operator=(const Employee &other);

private:
    QString myName;
    QDate myDateOfBirth;
};

如果我們沒有提供拷貝構函數或賦值運算符,C++會提供“一個值一個值地賦值”的默認實現。而且,如果你沒有提供任何構造函數,C++將提供一個默認的構造函數,使用默認構造函數初始化它的成員。雖然沒有任何顯式的構造函數或賦值運算符,下面的數據類型可以被存在容器中:

struct Movie
{
    int id;
    QString title;
    QDate releaseDate;
};

有些容器對它們能夠存儲的數據類型有特殊的要求,例如QMap<Key, T>鍵Key的必須提供<()運算符。在一些情況中,特定的函數有特殊的要求,達不到要求的話編譯器將會報錯。

Qt的容器提供運算符<<()和運算符>>(),這樣一來它們很容易使用QDataStream來讀寫,這意味着容器中的數據類型也必須支持運算符<<()和>>()。我們可以對上面的Movie類做一些事:

QDataStream &operator<<(QDataStream &out, const Movie &movie)
{
    out << (quint32)movie.id << movie.title
        << movie.releaseDate;
    return out;
}

QDataStream &operator>>(QDataStream &in, Movie &movie)
{
    quint32 id;
    QDate date;

    in >> id >> movie.title >> date;
    movie.id = (int)id;
    movie.releaseDate = date;
    return in;
}

 

迭代器類

迭代器提供了獲得容器中項的一套方法,Qt容器類有兩種類型的迭代器:Java風格的以及STL風格的。當調用非const的成員函數將容器中的數據從隱式共享的拷貝中修改或分離時,兩種迭代器都會失效。

Java風格的迭代器

Java風格的迭代器在Qt4中加入,比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<Key, T> QMutableMapIterator<Key, T>
QHash<Key, T>, QMultiHash<Key, T> QHashIterator<Key, T> QMutableHashIterator<Key, T>

 

在這里,我們只關注QList和QMap。QLinkedList、QVector和QSet與QList的迭代器有同樣的接口;QHash與QMap迭代器也有同樣的接口。

與STL風格的迭代器不同,Java風格的迭代器指向項之間的位置,而不是直接指向項。由於這個原因,它們指向第一項之前,或者最后一項之后,或者兩項之間。下面的圖展示了包含4項的list的有效的迭代器位置,用紅色箭頭表示:

下面是一個典型的例子,迭代器按順序循環遍歷QList<QString>的所有元素,並把它們打印到控制台上:

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

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

流程是這樣的:將要遍歷的Qlist被傳到QListIterator的構造函數,這時迭代器定位在list的第一項之前("A"之前),接下來我們調用hasNext()來檢測迭代器后面是否有一項,如果有,我們調用next()來跳過那一項,next()函數返回它跳過的那一項。對一個QList<QString>來說,那一項的類型是QString。

下面是如何在QList中倒序遍歷:

QListIterator<QString> i(list);
i.toBack();
while (i.hasPrevious())
    qDebug() << i.previous();

代碼和正序遍歷是對稱的,我們調用toBack()將迭代器移到最后一項后面的位置。

下圖描述了在一個迭代器上調用next()和previous()函數的效果:

下面的表概括了QListIterator的API:

函數 用途
toFront() 將迭代器移到list的最前面(在第一個項之前)
toBack() 將迭代器移到list的最后面 (最后一項之后)
hasNext() 如果迭代器沒有到list的最后則返回true
next() 返回下一項,並將迭代器向前移動一個位置
peekNext() 返回下一項,不會移動迭代器
hasPrevious() 如果迭代器沒有到list的最前面則返回true
previous() 返回上一項,並將迭代器移到上一個位置
peekPrevious() 返回上一項,不會移動迭代器

 

QListIterator沒有提供從list中插入或移除項的函數,想要實現插入和移除,你必須使用QMutableListIterator。下面舉例說明使用QMutableListIterator從QList<int>中移除所有奇數。

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

每次循環都會調用next(),它跳過list中的下一項,然后remove()函數移除我們剛剛從list中跳過的那一項,調用remove()不會使迭代器失效,所以它是安全的,我們可以繼續使用它。在倒序遍歷中同樣有效:

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

如果想修改某項的值,我們可以使用setValue(),下面的代碼中,我們用128來替換所以大於128的值:

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

和remove()一樣,setValue()操作我們剛剛跳過的那一項。如果是正序遍歷,這一項在當前迭代器之前;如果是倒序遍歷,這一項在當前迭代器之后。

next()函數返回list中這一項的非const引用,簡單點,我們甚至連setValue()都不需要:

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

正如上面所說,QLinkedList、QVector還有QSet的迭代器類和QList的迭代器有着相同的API。現在,我們來看看QMapIterator,有點不同,因為他在鍵值對上遍歷。

類似於QListIterator,QMapIterator提供了toFront()、toBack()、hasNext()、next()、peekNext()、hasPrevious()、previous()以及peekPrevious()。鍵和值的部分通過調用next()、peekNext()、previous()或peekPrevious()返回的對象的key()和value()來獲得。

下面的例子中,移除所有首都名字以“City”結尾的一對(capital, country):

QMap<QString, QString> map;
map.insert("Paris", "France");
map.insert("Guatemala City", "Guatemala");
map.insert("Mexico City", "Mexico");
map.insert("Moscow", "Russia");
...

QMutableMapIterator<QString, QString> i(map);
while (i.hasNext()) {
    if (i.next().key().endsWith("City"))
        i.remove();
}

QMapIterator還提供了直接在迭代器上操作的key()和value()函數,返回迭代器跳過的上一項的鍵和值。比如,下面的代碼把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());
}

如果想要使用同一個值遍歷所有項,我們使用findNext()或findPrevious()。下面例子中,我們移除所有帶有某個特定值的項:

QMutableMapIterator<int, QWidget *> i(map);
while (i.findNext(widget))
    i.remove();

STL風格的迭代器

自從Qt2.0發布就可以使用STL風格的迭代器了,它們適用於Qt和STL的泛型算法,並且對速度作了優化。

對於每個容器類,有兩種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迭代器的API是以數組中的指針為模型的,比如++運算符將迭代器前移到下一項,*運算符返回迭代器所指的那一項。事實上,對於QVector和QStack,它們的項在內存中存儲在相鄰的位置,迭代器類型正是T *,const迭代器類型正是const T *。

在討論中,我們重點放在QList和QMap,QLinkedList、QVector和QSet的迭代器類型與QList的迭代器有相同的接口;同樣地,QHash的迭代器類型與QMap的迭代器有相同的接口。

下面是一個典型例子,按順序循環遍歷QList<QString>中的所有元素,並將它們轉為小寫:

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()標記着一個無效的位置,不可以被解引用,主要用在循環的break條件。如果list是空的,begin()等於end(),所以我們永遠不會執行循環。

 下圖展示了一個包含4個元素的vector的所有有效迭代器位置,用紅色箭頭標出:

倒序遍歷需要我們在獲得項之前減少迭代器,這需要一個while循環:

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

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

在上面的代碼中,我們使用一元運算符*來獲得存儲在某個迭代器位置的項,然后我們調用QString::toLower(),大多數C++編譯器還允許我們使用i->toLower(),但有些不允許。

如果是只讀的,你可以使用const_iterator、constBegin()和constEnd(),比如:

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

下面的表概括了STL風格迭代器的API:

表達式 用途
*i 返回當前項
++i 將迭代器指向下一項
i += n 迭代器向前移動n項
--i 將迭代器指向上一項
i -= n 將迭代器你向后移動n項
i - j 返回迭代器i和j之間項的數目

 

++和--運算符可以使用前綴(++i, --i)和后綴(i++, i--)的形式,前綴的版本修改迭代器並返回修改后迭代器的引用,后綴版本在修改之前先復制迭代器,然后返回它的拷貝。在不需要考慮返回值的情況下,我們推薦使用前綴運算符(++i, --i),因為它們稍微快一點。

對於非const的迭代器類型,一元運算符*可以被用在賦值運算符的左邊。

對於QMap和QHash,*運算符返回項的值,如果你想要獲得鍵,只需在迭代器上調用key()。為了對稱,迭代器類型還提供了value()函數來獲得值。舉個例子,下面是如何將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包含很多返回QList或QStringList值的函數(比如QSplitter::sizes())。如果想要使用STL迭代器遍歷它們,你應該存儲一個拷貝,並在拷貝上進行遍歷。比如:

// RIGHT
const QList<int> sizes = splitter->sizes();
QList<int>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
    ...

// WRONG
QList<int>::const_iterator i;
for (i = splitter->sizes().begin();
        i != splitter->sizes().end(); ++i)
    ...

當函數返回容器的const或非const的引用,這個問題將不會發生。

 

foreach關鍵字

如果你想要按順序遍歷容器中的所有項,你可以使用Qt的foreach關鍵字。這個關鍵字是Qt特有的,與C++語言無關,並且使用了預處理器實現。

它的語法是:foreach (variable, container) statement。比如,下面是如何使用foreach遍歷QLinkedList<QString>:

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

foreach代碼明顯比使用迭代器的代碼少:

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

除非數據類型包含一個逗號(比如QPair<int, int>),用於遍歷的變量可以在foreach語句中定義:

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

和其它任何C++循環一樣,你可以在foreach循環中把主體放在括號里,而且你可以使用break來結束循環:

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

在QMap和QHash中,foreach可以獲得鍵值對中值的部分。如果你遍歷既想獲得鍵又想獲得值,則可以使用迭代器(這樣是最快的),或者你可以這樣寫:

QMap<QString, int> map;
...
foreach (const QString &str, map.keys())
    qDebug() << str << ":" << map.value(str);

對於一個多值的(multi-valued)map:

QMultiMap<QString, int> map;
...
foreach (const QString &str, map.uniqueKeys()) {
    foreach (int i, map.values(str))
        qDebug() << str << ":" << i;
}

當進入foreach循環時Qt自動獲得容器的一份拷貝,如果想修改你所遍歷的容器,並不會影響循環。

foreach創建了容器的一份拷貝,使用變量的非const引用可以禁止你修改最初的容器,但它會影響拷貝,這也許是你不願看到的。

 

其它類似容器的類

Qt提供了三個模板類,在一些方面與容器有點像。這些類不提供迭代器,而且不能使用foreach關鍵字。

  • QVarLengthArray<T, Prealloc>提供一個低級的可變長度的數組,當速度特別重要的時候,它可以被用來替換QVector。
  • QCache<Key, T>提供緩存,用來存儲和Key類型鍵相關聯的T類型的對象。
  • QContiguousCache<T>提供了一種緩存可連續獲得的數據的有效方式。
  • QPair<T1, T2>存儲一對元素。

其它類似於模板容器的非模板類型有QBitArray、QByteArray、QString和QStringList。

 

算法復雜度

算法復雜度關注當容器中項的數目增長時,函數有多快。例如,在QLinkedList中間插入一項是非常快的,無論其中存了多少項。另一方面,在QVector中項很多時,在中間插入一項是非常低效的,因為一半的項必須在內存中移動位置。

為了描述算法復雜度,我們使用下面的術語,基於“大O”標記法:

常量時間:O(1)。

指數時間:O(log n)。

線性時間:O(n)。

線性指數時間:O(nlog n)。

平方時間:O(n2)。

下面的表概括了Qt順序容器的算法復雜度:

  按索引查找 插入 在前面增加 在后面增加
QLinkedList<T> O(n) O(1) O(1) O(1)
QList<T> O(1) O(n) Amort. O(1) Amort. O(1)
QVector<T> O(1) O(n) O(n) Amort. O(1)

 

在表中,“Amort”指的是“平攤行為”。比如,“Amort.O(1)”指的是如果你只調用函數1次,你可能得到O(n),但如果你多次調用,平均下來將是O(1)。

下面的表概括了Qt關聯容器的算法復雜度:

  關鍵字查找 插入
平均 最壞情況 平均 最壞情況
QMap<Key, T> O(log n) O(log n) O(log n) O(log n)
QMultiMap<Key, T> O(log n) O(log n) O(log n) O(log n)
QHash<Key, T> Amort. O(1) O(n) Amort. O(1) O(n)
QSet<Key> Amort. O(1) O(n) Amort. O(1) O(n)

 

增長策略

QVector<T>、QString和 QByteArray在內存中連續存儲它們的項;QList<T>維護一個指向每一項指針的數組,從而提供快速的基於索引的獲得方法;QHash<Key, T>維護一個哈希表,它的大小與其中項的個數成比例。為了避免每次在容器末尾增加一項就分配一次內存,這些容器比實際需要的分配了更多的內存。

我們考慮下面的程序,根據一個QString來建立另一個QString:

QString onlyLetters(const QString &in)
{
    QString out;
    for (int j = 0; j < in.size(); ++j) {
        if (in[j].isLetter())
            out += in[j];
    }
    return out;
}

我們通過一次增加一個字符來動態地建立字符串。假設需要增加15000個字符,當字符串空間不夠時,將會發生18次重新分配內存:4, 8, 12, 16, 20, 52, 116, 244, 500, 1012, 2036, 4084, 6132, 8180, 10228, 12276, 14324, 16372。最后,QString有16372個Unicode字符被分配,15000個被占用。

這些值可能看起來有點奇怪,下面是增長的規則:

  • 1.QString一次分配4個字符,直到它增長到20。
  • 2.從20到4084,每次增長一倍,更准確地說,增長到下一個2的次方,減去12。
  • 3.從4084往后,每次增長2048個字符(4096字節)。這是因為當重新分配時,現代操作系統不會復制所有數據;物理內存被簡單地重新排序,只有第一頁和最后一頁的數據需要被拷貝。

QByteArray和QList<T>使用了與QString差不多的算法。

QVector<T>對一些數據類型也使用同樣的算法,這些數據類型可以使用memcpy()在內存中移動(包括基本的C++類型,指針類型以及Qt的共享類)。但是QVector<T>對只能調用構造和析構函數來移動的數據類型使用了不同的算法,這些情況下重新分配內存的代價更高,當空間不夠時,QVector<T>通過內存加一倍來減少再分配的次數。

QHash<Key, T>是一個完全不同的情況。QHash的內部哈希表以2的次方增長,每次增長時,項被定為到新的存儲塊中,通過qHash(key) % QHash::capacity()(存儲快的數目)計算。這個機制同樣適用於QSet<T>和QCache<Key, T>。

QVector<T>、QHash<Key, T>、QSet<T>、QString和QByteArray提供了一些函數,讓你能夠檢測和確定存儲這些項用了多少內存:

  • capacity()返回內存分配的項的數目(對QHash和QSet來說,是hash表中存儲塊的數目)。
  • reserve(size)顯式地為size個項預分配內存。
  • squeeze()釋放不需要用來存儲項的內存。

如果你知道在容器中大約要存儲多少項,可以調用reserve()開始,當你在容器中存儲結束,可以調用squeeze()來釋放額外的預分配的內存。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM