存储容器(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 提供了顺序存储容器:QList
,QLinkedList
,QVector
,QStack
和QQueue
。对于绝大多数应用程序,QList
是最好的选择。虽然它是基于数组实现的列表,但它提供了快速的向前添加和向后追加的操做。若是你须要链表,可使用QLinkedList
。若是你但愿全部元素占用连续地址空间,能够选择QVector
。QStack
和QQueue
则是 LIFO 和 FIFO 的。数据结构
Qt 还提供了关联容器:QMap
,QMultiMap
,QHash
,QMultiHash
和QSet
。带有“Multi”字样的容器支持在一个键上面关联多个值。“Hash”容器提供了基于散列函数的更快的查找,而非 Hash 容器则是基于二分搜索的有序集合。app
另外两个特例:QCache
和QContiguousCache
提供了在有限缓存空间中的高效 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 数据类型,例如QString
、QDate
和QTime
,都是可赋值数据类型。可是,QObject
及其子类(QWidget
、QTimer
等)都不是。也就是说,你不能使用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”):
QLinkedList::insert()
就是常量时间的。qBinaryFind()
就是对数时间的。QVector::insert()
就是线性时间的。基于上面的表示,咱们来看看 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) |
QVector
、QHash
和QSet
的头部添加是统计意义上的 O(log n)。然而,经过给定插入以前的元素个数来调用QVector::reserve()
、QHash::reserve()
和QSet::reserve()
,咱们能够把复杂度降到 O(1)。咱们会在下面详细讨论这个问题。
QVector<T>
、QString
和QByteArray
在连续内存空间中存储数据。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。QByteArray
和QList<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>
、QString
和QByteArray
提供了一系列函数,用于检测和指定究竟要分配多少内存:
capacity()
:返回实际已经分配内存的元素数目(对于QHash
和QSet
,则是散列表中桶的个数)reserve(size)
:为指定数目的元素显式地预分配内存。squeeze()
:释放那些不须要真实存储数据的内存空间。若是你知道容器大约有多少数据,那么你能够经过调用reserve()
函数来减小内存占用。若是已经将全部数据所有存入容器,则能够调用squeeze()
函数,释放全部未使用的预分配空间。