Qt提供了通用的基于模版实现方式的容器类:能做到通用性很大程度上就是依靠 模板 特性。容器本身就是为了存储数据的,但是这里的数据主要是在程序运行时,存储在内存中的数据,运行结束后就释放内存了。
Qt的内置容器特点:轻便、线程安全、相对于STL更容易使用。
Qt的容器是隐式共享的,它们是可重入的,并且它们针对速度、低内存消耗和最小的内联代码扩展进行了优化,从而产生了更小的可执行文件。此外,在它们被用于访问它们的所有线程用作只读容器的情况下,它们是线程安全的。
Qt支持两种访问容器数据的方式,提供了 Java-style 和 STL-style的迭代器,Java的那种方式更容易使用并高度功能化,同时STL的方式也很高效,并且支持STL的算法和容器结合的功能,异常强大。
Qt还提供了 foreach关键字,用来快速迭代整个容器(这个我经常使用)。
Qt提供了序列式容器: QList, QLinkedList, QVector, QStack, 以及QQueue.对于大多数应用开发来说,QList是最好的容器使用类型。QList被设计的其实是一个数组链表。因此它在链表头尾操作时有非常好效率。如果我们需要指针类型格式的链表,就要使用 QLinkedList。QVector的底层就是一个数组,那样它的内存和数组一样是连续的。常用的 LIFO(后入先出) 以及 FIFO(先入先出)等数据结果也有对应的容器:QStack、QQueue,也就是栈和堆。
Qt提供 关联式容器:QMap, QMultiMap, QHash, QMultiHash, 以及 QSet。带"Multi"容器可以方便地支持与单个键关联的多个值。哈希容器通过使用哈希函数而不是二进制搜索来对排序集提供更快的查找。
下面列出容器概述表
类 | 描述 |
---|---|
QList | 这个类最常用,Qt很多API的容器参数基本上都是QList类型的【因为这个类开销小,很多API默认参数基本就是QList类型】,因为QList 是使用数组实现的,数据之间有顺序的,也就支持索引的方式,从而确保基于索引的访问非常快。数组有的功能,QList都有,增删改查,数组的优缺点,QList也是一样的继承下来了。【优点:索引方便、首尾添加方便;缺点:数据量巨大时插入不方便】 |
QLinkedList | 类似QList,除了实现的方式是指针结构体形式,而不是数组形式外,其他在使用是没啥区别;学过数据结构的都知道指针链表的特点是啥,插入快,遍历慢【都是数据量大的时候】 |
QVector | 特点:节点内存连续,优缺点和数组一致,但是功能封装的比数组强大。 |
QStack | 其实就是QVector的子类,使用了QVector的局部功能,就是使用了栈的特性(LIFO) |
QQueue | 这个是QList的一个子类,使用的QList的局部功能,就是使用了队列的特性(FIFO) |
QSet | 子集的特性:全局保留一份数组,可以去重复。 |
QMap |
这提供了一个字典(关联数组),该字典将 Key 类型的键映射到 T 类型的值。通常每个键都与单个值相关联。QMap按键顺序存储其数据;如果顺序无关紧要,QHash是一个更快的选择。 QMap会排序键,通过键访问值。 |
QMultiMap |
字面来看,就是一个键能对应多个值,类似信号可以关联多个槽函数一样,一对多 |
QHash |
也是和QMap功能相似,只是通过哈希算法实现的键值匹配,而且匹配更快,但是键是无序排列 |
QMultiHash |
和QMultiMap功能一直,只是实现方式不一致,有自己的适应范围。 |
注意:
存储在各种容器中的值可以是任何可分配的数据类型。符合条件的数据类型必须能提供复制构造函数和赋值运算符。对于某些操作,还需要默认构造函数。这里涵盖了能被存储在容器中的大多数数据类型,包括基本类型(如 int 和 double等)、指针类型和 Qt 数据类型(如 QString、QDate 和 QTime等)。但是不包括QObject或任何QObject子类类型如(QWidget,QDialog,QTimer等)。 如果尝试实例化 QList< QWidget >,编译器将把 QWidget 的复制构造函数和赋值运算符禁用,然后这个容器就失效了。Qt为我们想到了另外一种方式:把这些类型的对象的地址存储在容器中,也就是将它们存储为指针类型,例如 QList
//不能使用这种方式
QList <QWidget> widgets;
//而是这样
QList<QWidget *> ptr_widgets;
迭代器提供了一种统一的方法来访问容器中的项目。Qt的容器类提供了两种类型的迭代器:Java风格的迭代器和STL风格的迭代器。
Java风格的迭代器在Qt 4中就被引入了,这种风格已经是Qt应用程序中使用的标准迭代器。它们比STL风格的迭代器更易于使用,但代价是效率略低。
对于每个容器类,有两种 Java 样式的迭代器数据类型:
容器名 | 只读 iterator | 读写 iterator |
---|---|---|
QList, QQueue | QListIterator | QMutableListIterator |
QLinkedList | QLinkedListIterator | QMutableLinkedListIterator |
QVector, QStack | QVectorIterator | QMutableVectorIterator |
QSet | QSetIterator | QMutableSetIterator |
QMap |
QMapIterator |
QMutableMapIterator |
QHash |
QHashIterator |
QMutableHashIterator |
学习的话要重点学习 QList 和 QMap。QLinkedList,QVector和QSet的迭代器类型与QList的迭代器具有完全相同的接口;同样的,QHash 的迭代器类型与 QMap 的迭代器具有相同的接口。
与 STL 样式的迭代器(如下所述)不同,Java 样式的迭代器指向项之间,而不是直接指向项。 因此,它们要么指向容器的最开头(在第一个项目之前),要么指向容器的最末尾(在最后一个项目之后),要么指向两个项目之间。下图显示了包含四个项目的列表的有效迭代器位置作为红色箭头:
如下面的代码一样:迭代器 i 是一个只读的迭代器,正向遍历 初始化后的位置是在 A 前面
//正向遍历
QList<QString> list;
list << "A" << "B" << "C" << "D";
QListIterator<QString> i(list);
while (i.hasNext())
qDebug() << i.next();
//反向遍历
QListIterator<QString> i(list);
i.toBack();
while (i.hasPrevious())
qDebug() << i.previous();
一些方法的示意样子图
下表总结了 QListIterator API:
功能 | 解释 |
---|---|
toFront() | 移动迭代器到可迭代对象的第一个位置(在节点最前) |
toBack() | 移动迭代器到可迭代对象的最后一个位置(在节点末尾) |
hasNext() | 判断可迭代对象是否有下一个对象 |
next() | 返回下一个对象,并移动迭代器到后一个位置 |
peekNext() | 返回下一个对象,但不移动迭代器 |
hasPrevious() | 判断可迭代对象是否有前一个对象 |
previous() | 返回前一个对象,并移动迭代器到前一个位置 |
peekPrevious() | 返回前一个对象,但不移动迭代器 |
这个函数名其实以及说的很清楚,大概知道意思,实在不知道再去查帮助文档。
面对要修改迭代对象时,就需要使用 QMutableListIterator 来完成
//删除节点
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() % 2 != 0)
i.remove();
}
//修改节点值 一
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() > 128)
i.setValue(128);
}
//修改节点值 二
QMutableListIterator<int> i(list);
while (i.hasNext())
i.next() *= 2;
我们现在将转向QMapIterator,它有些不同,因为它迭代(键,值)对。与 QListIterator 一样,QMapIterator 提供和QListIterator功能的函数如
QMap<QString, QString> map;
map.insert("Paris", "France");
map.insert("Guatemala City", "Guatemala");
map.insert("Mexico City", "Mexico");
map.insert("Moscow", "Russia");
QMapIterator<QString, QString> i(map);
while (i.hasNext()) {
qDebug()<<"key:"<<i.next().key()<<" value: "<<i.next().value();
}
特性就是能读能写,还能搭配能使用同类型迭代器使用
//删改示例
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();
}
//QHash和QMap交互
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 风格的迭代器自 Qt 2.0 发布以来就已经可用。它们与Qt和STL的通用算法兼容,并针对速度进行了优化。
对于每个容器类,有两种 STL 样式的迭代器类型:
形式上的区别就是看有没有加上 const_ 前缀
应尽可能使用只读迭代器,因为它们比读写迭代器更快。
容器名 | 只读 iterator | 读写 iterator |
---|---|---|
QList, QQueue | QList::const_iterator | QList::iterator |
QLinkedList | QLinkedList::const_iterator | QLinkedList::iterator |
QVector, QStack | QVector::const_iterator | QVector::iterator |
QSet | QSet::const_iterator | QSet::iterator |
QHash |
QHash |
QHash |
STL 迭代器的 API 以数组中的指针为模型。例如,++ 运算符将迭代器前进到下一个项,* 运算符返回迭代器指向的项。实际上,对于将项目存储在相邻内存位置的 QVector 和 QStack,迭代器类型只是 T * 的 typedef,而 const_iterator 类型只是 const T * 的 typedef。
重点介绍 QList 和 QMap。QLinkedList,QVector和QSet的迭代器类型与QList的迭代器具有完全相同的接口;同样,QHash 的迭代器类型与 QMap 的迭代器具有相同的接口。
迭代器指向的节点值就是一个一个节点类型。
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(),因此我们从不执行循环。
在迭代时,关于迭代的item如何获取,以及如何跨步长来获取,看下表
操作 | 含义 |
---|---|
*i | 返回当前节点 |
++i | 迭代器后移一位 |
i += n | 迭代器后移 n 位 |
–i | 迭代器前移一位 |
i -= n | 迭代器前移 n 位 |
i - j | 两个迭代器直接的索引差 |
对于QMap和QHash,*i 返回项的值和Java-style迭代器返回是一样的 键值对 。要获取迭代器节点的键 需要调用key()。同样的迭代器类型还提供了一个value()函数来获取值。其实很像QList里面存放结构体类型一样。
QMap<int, int> map;
QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i)
qDebug() << i.key() << ':' << i.value();
例子一:
像这样的写法 不对
QVector<int> a, b;
a.resize(100000); // make a big vector filled with 0.
QVector<int>::iterator i = a.begin();
// WRONG way of using the iterator i:
b = a;
a[0] = 5;
b.clear(); // Now the iterator i is completely invalid.
int j = *i; // Undefined behavior!
最好改为这样的
QVector<int>::iterator i = a.begin();
b = a;
QVector<int>::iterator i = a.begin();
a[0] = 5;
b.clear();
int j = *i;
例子二
// 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)
如果你只想按顺序遍历容器中的所有项,你可以使用Qt的foreach关键字。该关键字是c++语言中特定于qt的附加内容,是使用预处理器实现的。
用法很简单:
foreach (variable, container)
{
//操作
}
操作里面包括:获取值节点,break退出循环 都可以
//例子一
QLinkedList<QString> list;
QString str;
foreach (str, list)
qDebug() << str;
//例子二
QLinkedList<QString> list;
foreach (const QString &str, list)
qDebug() << str;
//例子三
QLinkedList<QString> list;
foreach (const QString &str, list) {
if (str.isEmpty())
break;
qDebug() << str;
}
直接上例子。没啥特殊的
QMap<QString, int> map;
foreach (const QString &str, map.keys())
qDebug() << str << ':' << map.value(str);
//多键值对映射时
QMultiMap<QString, int> map;
foreach (const QString &str, map.uniqueKeys()) {
foreach (int i, map.values(str))
qDebug() << str << ':' << i;
}
当Qt进入foreach循环时,它会自动获取容器的副本【也就是会拷贝一份副本】。如果你在迭代的时候修改了容器,那不会影响循环。(如果不修改容器,复制仍然会发生,但是由于Qt使用了隐式共享,复制容器的操作非常快)
Qt的foreach循环的替代方法是基于范围的for,它是C++ 11和更新版本的一部分。但是,请记住,基于范围的for可能会强制分离Qt容器,而foreach不会。
Qt包括三个模板类,在某些方面类似于容器。这些下面这些类不提供迭代器,不能与foreach关键字一起使用。
变量名 | 描述 |
---|---|
QVarLengthArray |
提供了一个低级的可变长度数组。在速度特别重要的地方可以代替QVector使用。 |
QCache |
提供了一个缓存来存储与Key类型的键相关联的某个T类型的对象。 |
QContiguousCache < T > | 提供了一种高效的方法来缓存通常以连续方式访问的数据。 |
QPair |
存储一对元素 |
Qt的中存在的非通用模板类型有QBitArray、QByteArray、QString和QStringList。这些类存储的都是特定类型的item,虽然能使用迭代器的方法,但是已经失去通用容器的意义了。
算法复杂性关注的是随着容器中项目数量的增加,每个函数的速度有多快(或多慢)。例如,在QLinkedList的中间插入一个项目是一个非常快速的操作,与QLinkedList中存储的项目数量无关。另一方面,如果QVector包含许多项,那么在QVector中间插入一个项开销可能非常大,因为必须将一半的项在内存中移动一个位置。
“Amort ”代表“分摊行为”。比如“Amort.O(1)”意味着如果只调用该函数一次,可能会得到O(n)行为,但是如果调用它多次(例如,n次),平均行为将是O(1)。
Qt的序列式容器复杂度
变量名 | 查找 | 插入 | 头部插入 | 尾部插入 |
---|---|---|---|---|
QLinkedList | O(n) | O(1) | O(1) | O(1) |
QList | O(1) | O(n) | Amort. O(1) | Amort. O(1) |
QVector | O(1) | O(n) | O(n) | Amort. O(1) |
Qt的关联式容器以及集合复杂度
变量名 | 键 查找平均情况 | 键 查找最坏情况 | 插入平均情况 | 插入最坏情况 |
---|---|---|---|---|
QMap |
O(log n) | O(log n) | O(log n) | O(log n) |
QMultiMap |
O(log n) | O(log n) | O(log n) | O(log n) |
QHash |
Amort. O(1) | O(n) | Amort. O(1) | O(n) |
QSet | Amort. O(1) | O(n) | Amort. O(1) | O(n) |
QVector、QString和QByteArray在内存中连续存储它们的项;QList维护一个指向其存储的项的指针数组,以提供基于索引的快速访问(除非T是指针类型或指针大小的基本类型,在这种情况下,值本身存储在数组中);QHash
为了避免每次在容器末尾添加项目时都重新分配数据,这些类通常会分配多的一些的内存。
假设我们向QString字符串追加了15000个字符。那么当QString用尽空间时,将发生以下18次重新分配(总共可能有15000次): 4、8、12、16、20、52、116、244、500、1012、2036、4084、6132、8180、10228、12276、14324、16372。最后,QString分配了16372个Unicode字符,其中15000个被占用。
上面的值可能看起来有点奇怪,但以下是指导原则:
QByteArray 和 QList跟QString在分配上是大同小异的。
但是若 QVector 也将该算法用于可以使用memcpy()在内存中移动的数据类型(包括基本的C++类型、指针类型和Qt的共享类),但对只能通过调用 复制构造函数 和 析构函数 来移动。由于在这种情况下重新分配的成本更高,所以QVector通过在空间用尽时总是将内存加倍来减少重新分配的次数。
QHash
对于大多数应用程序来说,Qt提供的默认增长算法可以解决这个问题。如果您需要更多的控制,QVector、QHash
函数名 | 描述 |
---|---|
capacity() | 返回容器中所有节点所占的空间大小 |
reserve(size) | 为容器显式的预分配内存大小 |
squeeze() | 释放掉没有使用的容器空间,缩小容器的大小 |