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 提供了顺序存储容器:QList,QLinkedList,QVector,QStack和QQueue。对于绝大多数应用程序,QList是最好的选择。虽然它是基于数组实现的列表,但它提供了快速的向前添加和向后追加的操作。如果你需要链表,可以使用QLinkedList。如果你希望所有元素占用连续地址空间,可以选择QVector。QStack和QQueue则是 LIFO 和 FIFO 的。
Qt 还提供了关联容器:QMap,QMultiMap,QHash,QMultiHash和QSet。带有“Multi”字样的容器支持在一个键上面关联多个值。“Hash”容器提供了基于散列函数的更快的查找,而非 Hash 容器则是基于二分搜索的有序集合。
另外两个特例: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++ 编译器会将连续的两个 > 当做输入重定向运算符,因此,这里的两个 > 中间必须有一个空格。
能够存储在容器中的数据必须是可赋值数据类型。所谓可赋值数据类型,是指具有默认构造函数、拷贝构造函数和赋值运算符的类型。绝大多数数据类型,包括基本类型,比如 int 和 double,指针,Qt 数据类型,例如QString、QDate和QTime,都是可赋值数据类型。但是,QObject及其子类(QWidget、QTimer等)都不是。也就是说,你不能使用QList<QWidget>这种容器,因为QWidget的拷贝构造函数和赋值运算符不可用。如果你需要这种类型的容器,只能存储其指针,也就是QList<QWidget *>。
如果要使用QMap或者QHash,作为键的类型必须提供额外的辅助函数。QMap的键必须提供operator<()重载,QHash的键必须提供operator==()重载和一个名字是qHash()的全局函数。
作为例子,我们考虑如下的代码:
|
1
2
3
4
5
6
|
struct Movie
{
int id;
QString title;
QDate releaseDate;
};
|
作为 struct,我们当做纯数据类使用。这个类没有额外的构造函数,因此编译器会为我们生成一个默认构造函数。同时,编译器还会生成默认的拷贝构造函数和赋值运算符。这就满足了将其放入容器类存储的条件:
|
1
|
QList<Movie> movs;
|
Qt 容器类可以直接使用QDataStream进行存取。此时,容器中所存储的类型必须也能够使用QDataStream进行存储。这意味着,我们需要重载operator<<()和operator>>()运算符:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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) |
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>维护一张散列表,其大小与散列中数据量相同。为避免每次插入数据都要重新分配数据空间,这些类都提供了多余实际值的数据位。
我们通过下面的代码来了解这一算法:
|
1
2
3
4
5
6
7
8
9
|
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 字节)。这是有特定意义的,因为现代操作系统在重新分配一个缓存时,不会复制整个数据;物理内存页只是简单地被重新排序,只有第一页和最后一页的数据会被复制。
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()函数,释放所有未使用的预分配空间。
Qt 学习之路:存储容器的更多相关文章
- Qt 学习之路 2(38):存储容器
Qt 学习之路 2(38):存储容器 豆子 2013年1月14日 Qt 学习之路 2 38条评论 存储容器(containers)有时候也被称为集合(collections),是能够在内存中存储其它特 ...
- Qt 学习之路 2(39):遍历容器
Qt 学习之路 2(39):遍历容器 豆子 2013年1月16日 Qt 学习之路 2 29条评论 上一节我们大致了解了有关存储容器的相关内容.对于所有的容器,最常用的操作就是遍历.本章我们将详细了解有 ...
- Qt 学习之路 2(40):隐式数据共享
Qt 学习之路 2(40):隐式数据共享 豆子 2013年1月21日 Qt 学习之路 2 14条评论 Qt 中许多 C++ 类使用了隐式数据共享技术,来最大化资源利用率和最小化拷贝时的资源消耗.当作为 ...
- 《Qt 学习之路 2》目录
<Qt 学习之路 2>目录 <Qt 学习之路 2>目录 豆子 2012年8月23日 Qt 学习之路 2 177条评论 <Qt 学习之路 2>目录 序 Qt ...
- Qt 学习之路 2(66):访问网络(2)
Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2) 豆子 2013年10月31日 Qt 学习之路 2 27条评论 上一 ...
- Qt 学习之路 2(59):使用流处理 XML
Qt 学习之路 2(59):使用流处理 XML 豆子 2013年7月25日 Qt 学习之路 2 18条评论 本章开始我们将了解到如何使用 Qt 处理 XML 格式的文档. XML(eXtensible ...
- Qt 学习之路 2(53):自定义拖放数据
Qt 学习之路 2(53):自定义拖放数据 豆子 2013年5月26日 Qt 学习之路 2 13条评论上一章中,我们的例子使用系统提供的拖放对象QMimeData进行拖放数据的存储.比如使用QM ...
- Qt 学习之路 2(52):使用拖放
Qt 学习之路 2(52):使用拖放 豆子 2013年5月21日 Qt 学习之路 2 17条评论 拖放(Drag and Drop),通常会简称为 DnD,是现代软件开发中必不可少的一项技术.它提供了 ...
- Qt 学习之路 2(51):布尔表达式树模型
Qt 学习之路 2(51):布尔表达式树模型 豆子 2013年5月15日 Qt 学习之路 2 17条评论 本章将会是自定义模型的最后一部分.原本打算结束这部分内容,不过实在不忍心放弃这个示例.来自于 ...
- Qt 学习之路 2(50):自定义可编辑模型
Home / Qt 学习之路 2 / Qt 学习之路 2(50):自定义可编辑模型 Qt 学习之路 2(50):自定义可编辑模型 豆子 2013年5月13日 Qt 学习之路 2 13条评论 上一章我们 ...
随机推荐
- 为什么我们不喜欢用富UI控件
我们对于理解一般意义的抽象关系并没有问题,但如第一部分使用Entity Framework 时说明的,事实恰好相反.我们还喜欢在交付应用程序时利用抽象关系,比如使用Azure等云服务.在这两种情况下, ...
- php报警:Strict Standards: Only variables should be passed by reference in
错误原因 因为end函数的原因. end函数: mixed end ( array &$array ) 你可以看到end的参数是一个引用(reference),而你只能把一个变量的引 ...
- python读写Excel文件的函数--使用xlrd/xlwt
python中读取Excel的模块或者说工具有很多,如以下几种: Packages 文档下载 说明 openpyxl Download | Documentation | Bitbucket The ...
- Python【第七篇】面向对象进阶
大纲 一.面向对象高级语法 1.静态方法.类方法.属性方法 2.类的特殊成员方法 3.反射 二.异常处理 三.网络编程之socket基础 一.面向对象高级语法 1.静态方法:名义上归类管理,实际上静态 ...
- jquery ZeroClipboard实现黏贴板功能,兼容所有浏览器
两天前听了一个H5的分享,会议上有一句话,非常有感触:不是你不能,而是你对自己的要求太低.很简单的一句话,相信很多事情不是大家做不到,真的是对自己的要求太低,如果对自己要求多一点,那么你取得的进步可能 ...
- bzoj1662: [Usaco2006 Nov]Round Numbers 圆环数
Description 正如你所知,奶牛们没有手指以至于不能玩“石头剪刀布”来任意地决定例如谁先挤奶的顺序.她们甚至也不能通过仍硬币的方式. 所以她们通过"round number" ...
- Laravel框架——增删改查
增: //save返回true false $res = new member(); res->username = 'a'; $res->password = 'b'; dd($res- ...
- Ruby自学笔记(五)— 条件判断
条件判断,在编程语言中都存在,而Ruby中的条件判断和Java中类似,当然还是存在些许差别 Ruby中条件判断的条件: 1) 可以使用 ==,<,>等比较运算来作为条件,比较运算可以返回t ...
- Unity3d 读取网络xml
Unity3d 读取网络xml Unity3d 读取网络xml,这个xml文件需要不包含BOM信息,可以用UltraEdit打开xml文件,并且另存为的时候,选择不包含BOM的utf-8格式存储!
- bzoj1221
网络流与线性规划24题中的餐巾计划吧明显要拆点吧,把每一天拆成2个点,i,i+n起点 终点 容量 费用 s i inf c 每天都可以购买新毛巾 i ...