【Qt筆記】存儲容器

存儲容器(containers)有時候也被稱爲集合(collections),是可以在內存中存儲其它特定類型的對象,一般是一些經常使用的數據結構,通常是通用模板類的形式。C++ 提供了一套完整的解決方案,做爲標準模板庫(Standard Template Library)的組成部分,也就是常說的 STL。算法

Qt 提供了另一套基於模板的容器類。相比 STL,這些容器類一般更輕量、更安全、更容易使用。若是你對 STL 不大熟悉,或者更喜歡 Qt 風格的 API,那麼你就應該選擇使用這些類。固然,你也能夠在 Qt 中使用 STL 容器,沒有任何問題。數組

本章的目的,是讓你可以選擇使用哪一個容器,而不是告訴你這個類都哪些函數。這個問題能夠在文檔中找到更清晰的回答。緩存

 

Qt 的容器類都不繼承QObject,都提供了隱式數據共享、不可變的特性,而且爲速度作了優化,具備較低的內存佔用量等。另一點比較重要的,它們是線程安全的。這些容器類是平臺無關的,即不因編譯器的不一樣而具備不一樣的實現;隱式數據共享,有時也被稱做「寫時複製(copy on write)」,這種技術容許在容器類中使用傳值參數,但卻不會出現額外的性能損失。遍歷是容器類的重要操做。Qt 容器類提供了相似 Java 的遍歷器語法,一樣也提供了相似 STL 的遍歷器語法,以方便用戶選擇本身習慣的編碼方式。相比而言,Java 風格的遍歷器更易用,是一種高層次的函數;而 STL 風格的遍歷器更高效,同時可以支持 Qt 和 STL 的通用算法。最後一點,在一些嵌入式平臺,STL 每每是不可用的,這時你就只能使用 Qt 提供的容器類,除非你想本身建立。順便提一句,除了遍歷器,Qt 還提供了本身的 foreach 語法(C++ 11 也提供了相似的語法,但有所區別,詳見這裏的 foreach 循環一節)。安全

Qt 提供了順序存儲容器:QListQLinkedListQVectorQStackQQueue。對於絕大多數應用程序,QList是最好的選擇。雖然它是基於數組實現的列表,但它提供了快速的向前添加和向後追加的操做。若是你須要鏈表,可使用QLinkedList。若是你但願全部元素佔用連續地址空間,能夠選擇QVectorQStackQQueue則是 LIFO 和 FIFO 的。數據結構

Qt 還提供了關聯容器:QMapQMultiMapQHashQMultiHashQSet。帶有「Multi」字樣的容器支持在一個鍵上面關聯多個值。「Hash」容器提供了基於散列函數的更快的查找,而非 Hash 容器則是基於二分搜索的有序集合。app

另外兩個特例:QCacheQContiguousCache提供了在有限緩存空間中的高效 hash 查找。函數

咱們將 Qt 提供的各個容器類總結以下:性能

  • QList<T>:這是至今爲止提供的最通用的容器類。它將給定的類型 T 的對象以列表的形式進行存儲,與一個整型的索引關聯。QList在內部使用數組實現,同時提供基於索引的快速訪問。咱們可使用 QList::append()QList::prepend()在列表尾部或頭部添加元素,也可使用QList::insert()在中間插入。相比其它容器類,QList專門爲這種修改操做做了優化。QStringList繼承自QList<QString>
  • QLinkedList<T>:相似於 QList,除了它是使用遍歷器進行遍歷,而不是基於整數索引的隨機訪問。對於在中部插入大量數據,它的性能要優於QList。同時具備更好的遍歷器語義(只要數據元素存在,QLinkedList的遍歷器就會指向一個合法元素,相比而言,當插入或刪除數據時,QList的遍歷器就會指向一個非法值)。
  • QVector<T>:用於在內存的連續區存儲一系列給定類型的值。在頭部或中間插入數據可能會很是慢,由於這會引發大量數據在內存中的移動。
  • QStack<T>:這是QVector的子類,提供了後進先出(LIFO)語義。相比QVector,它提供了額外的函數:push()pop()top()
  • QQueue<T>:這是QList的子類,提供了先進先出(FIFO)語義。相比QList,它提供了額外的函數:enqueue()dequeue()head()
  • QSet<T>:提供單值的數學上面的集合,具備快速的查找性能。
  • QMap<Key, T>:提供了字典數據結構(關聯數組),將類型 T 的值同類型 Key 的鍵關聯起來。一般,每一個鍵與一個值關聯。QMap以鍵的順序存儲數據;若是順序無關,QHash提供了更好的性能。
  • QMultiMap<Key, T>:這是QMap的子類,提供了多值映射:一個鍵能夠與多個值關聯。
  • QHash<Key, T>:該類同QMap的接口幾乎相同,可是提供了更快的查找。QHash以字母順序存儲數據。
  • QMultiHash<Key, T>:這是QHash的子類,提供了多值散列。

全部的容器均可以嵌套。例如,QMap<QString, QList<int> >是一個映射,其鍵是QString類型,值是QList<int>類型,也就是說,每一個值均可以存儲多個 int。這裏須要注意的是,C++ 編譯器會將連續的兩個 > 當作輸入重定向運算符,所以,這裏的兩個 > 中間必須有一個空格。(在C++11中能夠連續使用)。優化

可以存儲在容器中的數據必須是可賦值數據類型。所謂可賦值數據類型,是指具備默認構造函數、拷貝構造函數和賦值運算符的類型。絕大多數數據類型,包括基本類型,好比 int 和 double,指針,Qt 數據類型,例如QStringQDateQTime,都是可賦值數據類型。可是,QObject及其子類(QWidgetQTimer等)都不是。也就是說,你不能使用QList<QWidget>這種容器,由於QWidget的拷貝構造函數和賦值運算符不可用。若是你須要這種類型的容器,只能存儲其指針,也就是QList<QWidget *>ui

若是要使用QMap或者QHash,做爲鍵的類型必須提供額外的輔助函數。QMap的鍵必須提供operator<()重載,QHash的鍵必須提供operator==()重載和一個名字是qHash()的全局函數。

做爲例子,咱們考慮以下的代碼:

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

做爲 struct,咱們當作純數據類使用。這個類沒有額外的構造函數,所以編譯器會爲咱們生成一個默認構造函數。同時,編譯器還會生成默認的拷貝構造函數和賦值運算符。這就知足了將其放入容器類存儲的條件:

QList<Movie> movs;

Qt 容器類能夠直接使用QDataStream進行存取。此時,容器中所存儲的類型必須也可以使用QDataStream進行存儲。這意味着,咱們須要重載operator<<()operator>>()運算符:

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;
}

根據數據結構的相關內容,咱們有必要對這些容器類的算法複雜性進行定量分析。算法複雜度關心的是在數據量增加時,容器的每個函數究竟有多快(或者多慢)。例如,向QLinkedList中部插入數據是一個至關快的操做,而且與QLinkedList中已經存儲的數據量無關。另外一方面,若是QVector中已經保存了大量數據,向QVector中部插入數據會很是慢,由於在內存中,有一半的數據必須移動位置。爲了描述算法複雜度,咱們引入 O 記號(大寫字母 O,讀做「大 O」):

  • 常量時間:O(1)。若是一個函數的運行時間與容器中數據量無關,咱們說這個函數是常量時間的。QLinkedList::insert()就是常量時間的。
  • 對數時間:O(log n)。若是一個函數的運行時間是容器數據量的對數關係,咱們說這個函數是對數時間的。qBinaryFind()就是對數時間的。
  • 線性時間:O(n)。若是一個函數的運行時間是容器數據量的線性關係,也就是說直接與數量相關,咱們說這個函數是限行時間的。QVector::insert()就是線性時間的。
  • 線性對數時間:O(n log n)。線性對數時間要比線性時間慢,可是要比平方時間快。
  • 平方時間:O(n²)。平方時間與容器數據量的平方關係。

基於上面的表示,咱們來看看 Qt 順序容器的算法複雜度:

  查找 插入 前方添加 後方追加
QLinkedList<T> O(n) O(1) O(1) O(1)
QList<T> O(1) O(n) 統計 O(1) 統計 O(1)
QVector<T> O(1) O(n) O(n) 統計 O(1)

上表中,所謂「統計」,意思是統計意義上的數據。例如「統計 O(1)」是說,若是隻調用一次,其運行時間是 O(n),可是若是調用屢次(例如 n 次),則平均時間是 O(1)。

下表則是關聯容器的算法複雜度:

  查找鍵 插入
平均 最壞 平均 最壞
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> 統計 O(1) O(n) O(1) 統計 O(n)
QSet<Key, T> 統計 O(1) O(n) O(1) 統計 O(n)

QVectorQHashQSet的頭部添加是統計意義上的 O(log n)。然而,經過給定插入以前的元素個數來調用QVector::reserve()QHash::reserve()QSet::reserve(),咱們能夠把複雜度降到 O(1)。咱們會在下面詳細討論這個問題。

QVector<T>QStringQByteArray在連續內存空間中存儲數據。QList<T>維護指向其數據的指針數組,提供基於索引的快速訪問(若是 T 就是指針類型,或者是與指針大小相同的其它類型,那麼 QList 的內部數組中存的就是其實際值,而不是其指針)。QHash<Key, T>維護一張散列表,其大小與散列中數據量相同。爲避免每次插入數據都要從新分配數據空間,這些類都提供了多餘實際值的數據位。

咱們經過下面的代碼來了解這一算法:

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。最後,這個 out 對象一共有 16372 個 Unicode 字符,其中 15000 個是有實際數據的。

上面的分配數據有些奇怪,實際上是有章可循的:

  • QString每次分配 4 個字符,直到達到 20。
  • 在 20 到 4084 期間,每次分配大約一倍。準確地說,每次會分配下一個 2 的冪減 12。(某些內存分配器在分配 2 的冪數時會有很是差的性能,由於他們會佔用某些字節作預訂)
  • 自 4084 起,每次多分配 2048 個字符(4096 字節)。這是有特定意義的,由於現代操做系統在從新分配一個緩存時,不會複製整個數據;物理內存頁只是簡單地被從新排序,只有第一頁和最後一頁的數據會被複制。

QByteArrayQList<T>實際算法與QString很是相似。

對於那些可以使用memcry()(包括基本的 C++ 類型,指針類型和 Qt 的共享類)函數在內存中移動的數據類型,QVector<T>也使用了相似的算法;對於那些只能使用拷貝構造函數和析構函數才能移動的數據類型,使用的是另一套算法。因爲後者的消耗更高,因此QVector<T>減小了超出空間時每次所要分配的額外內存數。

QHash<Key, T>則是徹底不一樣的形式。QHash的內部散列表每次會增長 2 的冪數;每次增長時,全部數據都要從新分配到新的桶中,其計算公式是qHash(key) % QHash::capacity()QHash::capacity()就是桶的數量)。這種算法一樣適用於 QSet<T>QCache<Key, T>。若是你不明白「桶」的概念,請查閱數據結構的相關內容。

對於大多數應用程序。Qt 默認的增加算法已經足夠。若是你須要額外的控制,QVector<T>QHash<Key, T>QSet<T>QStringQByteArray提供了一系列函數,用於檢測和指定究竟要分配多少內存:

  • capacity():返回實際已經分配內存的元素數目(對於QHashQSet,則是散列表中桶的個數)
  • reserve(size):爲指定數目的元素顯式地預分配內存。
  • squeeze():釋放那些不須要真實存儲數據的內存空間。

若是你知道容器大約有多少數據,那麼你能夠經過調用reserve()函數來減小內存佔用。若是已經將全部數據所有存入容器,則能夠調用squeeze()函數,釋放全部未使用的預分配空間。

相關文章
相關標籤/搜索