容器类
介绍
Qt 库提供了一组通用的基于模板的容器类。这些类可以用于存储指定类型的项目。例如,如果需要一个可调整大小的 QString 数组,可以使用 QVector。
这些容器类设计得比 STL 容器更轻量、更安全且更易于使用。如果你不熟悉 STL,或者更喜欢“Qt 方式”的做法,可以使用这些类代替 STL 类。
容器类是隐式共享的,它们是可重入的,并且经过优化以达到快速、低内存消耗和最小的内联代码扩展,从而生成更小的可执行文件。此外,在所有线程都将它们用作只读容器的情况下,它们是线程安全的。
为了遍历容器中存储的项目,你可以使用两种类型的迭代器:Java 风格的迭代器和 STL 风格的迭代器。Java 风格的迭代器更易于使用,并提供高级功能,而 STL 风格的迭代器效率略高,并且可以与 Qt 和 STL 的泛型算法一起使用。
Qt 还提供了一个 foreach 关键字,使得遍历容器中所有项目非常容易。
注意: 自 Qt 5.14 起,大多数容器类都提供了范围构造函数。QMultiMap 是一个显著的例外。建议使用这些范围构造函数替代各种 from/to 方法。例如:
QVector<int> vector{1, 2, 3, 4, 4, 5};
QSet<int> set(vector.begin(), vector.end());
/*
将生成一个包含 1, 2, 4, 5 的 QSet。
*/
容器类
Qt 提供了以下顺序容器:QList、QLinkedList、QVector、QStack 和 QQueue。对于大多数应用程序,QList 是最好的选择。尽管它实现为一个数组列表,但它提供了非常快的前置和追加操作。如果你真的需要一个链表,请使用 QLinkedList;如果希望项目占据连续的内存位置,请使用 QVector。QStack 和 QQueue 是提供 LIFO 和 FIFO 语义的便利类。
Qt 还提供了以下关联容器:QMap、QMultiMap、QHash、QMultiHash 和 QSet。“Multi” 容器方便地支持与单个键关联的多个值。“Hash” 容器通过使用哈希函数而不是对排序集进行二进制搜索,提供了更快的查找。
作为特例,QCache 和 QContiguousCache 类提供了在有限缓存存储中高效的哈希查找对象。
类 | 概要 |
---|---|
QList | 这是目前使用最广泛的容器类。它存储给定类型 (T) 的值列表,这些值可以通过索引访问。内部实现为数组,确保了基于索引的访问非常快。可以使用 QList::append() 和 QList::prepend() 在列表的任一端添加项目,或者使用 QList::insert() 在中间插入项目。QList 是高度优化的,能够在可执行文件中扩展为尽可能少的代码。QStringList 继承自 QList。 |
QLinkedList | 这类似于 QList,不同之处在于它使用迭代器而不是整数索引来访问项目。在中间插入大量项目时,性能优于 QList,并且具有更好的迭代器语义(指向 QLinkedList 项目的迭代器在项目存在期间保持有效,而指向 QList 的迭代器在任何插入或删除操作后都可能失效)。 |
QVector | 以连续的内存位置存储给定类型的值数组。在前端或中间插入可能会很慢,因为这可能导致大量项目在内存中移动一个位置。 |
QVarLengthArray<T, Prealloc> | 提供低级可变长度数组。它可以在速度特别重要的地方代替 QVector 使用。 |
QStack | 是 QVector 的便利子类,提供“后进先出”(LIFO)语义。它添加了以下函数:push()、pop() 和 top()。 |
QQueue | 是 QList 的便利子类,提供“先进先出”(FIFO)语义。它添加了以下函数:enqueue()、dequeue() 和 head()。 |
QSet | 提供单值数学集,查找速度快。 |
QMap<Key, T> | 提供将键类型 Key 映射到值类型 T 的字典(关联数组)。通常每个键都与一个值关联。QMap 按键顺序存储数据;如果顺序无关紧要,QHash 是更快的替代方案。 |
QMultiMap<Key, T> | 是 QMap 的便利子类,提供多值映射的良好接口,即一个键可以与多个值关联的映射。 |
QHash<Key, T> | 拥有与 QMap 几乎相同的 API,但提供显著更快的查找。QHash 以任意顺序存储数据。 |
QMultiHash<Key, T> | 是 QHash 的便利子类,提供多值哈希的良好接口。 |
容器可以嵌套使用。例如,完全可以使用 QMap<QString, QList>,其中键类型为 QString,值类型为 QList。
这些容器在与容器同名的各自头文件中定义(例如,)。为了方便起见,这些容器在 中进行了前向声明。
存储在各种容器中的值可以是任何可分配的数据类型。要符合条件,类型必须提供复制构造函数和赋值操作符。有些操作还需要默认构造函数。这涵盖了你可能想要存储在容器中的大多数数据类型,包括基本类型(如 int 和 double)、指针类型以及 Qt 数据类型(如 QString、QDate 和 QTime),但不包括 QObject 或任何 QObject 子类(QWidget、QDialog、QTimer 等)。如果你尝试实例化 QList,编译器会提示 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 类型必须提供 operator<()
. 这种特殊要求在类的详细描述中记录。在某些情况下,特定函数有特殊要求;这些要求在每个函数的基础上描述。如果不满足要求,编译器总会发出错误。
Qt 的容器提供了 operator<<() 和 operator>>(),以便可以使用 QDataStream 轻松地读取和写入。这意味着存储在容器中的数据类型也必须支持 operator<<() 和 operator>>(). 提供这种支持很简单;以下是我们如何为上述 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;
}
某些容器类函数的文档提到默认构造的值;例如,QVector 自动使用默认构造的值初始化其项目,而 QMap::value() 如果指定键不在映射中则返回默认构造的值。对于大多数值类型,这只是意味着使用默认构造函数创建一个值(例如,对于 QString,是一个空字符串)。但对于原始类型(如 int 和 double)以及指针类型,C++ 语言没有指定任何初始化;在这些情况下,Qt 的容器会自动将值初始化为 0。
迭代器类
迭代器提供了统一的访问容器中项的方式。Qt 的容器类提供了两种类型的迭代器:Java 风格迭代器和 STL 风格迭代器。当容器中的数据被修改或由于调用非 const 成员函数而从隐式共享的副本中分离时,这两种类型的迭代器都会失效。
Java 风格迭代器
Java 风格迭代器是 Qt 4 中新增的,是 Qt 应用程序中常用的标准迭代器。它们比 STL 风格迭代器更方便使用,但效率略低。它们的 API 基于 Java 的迭代器类。
对于每个容器类,都有两种 Java 风格迭代器数据类型:一种提供只读访问,另一种提供读写访问。
容器 | 只读迭代器 | 读写迭代器 |
---|---|---|
QList, QQueue | QListIterator | QMutableListIterator |
QLinkedList | QLinkedListIterator | QMutableLinkedListIterator |
QVector, QStack | QVectorIterator | QMutableVectorIterator |
QSet | QSetIterator | QMutableSetIterator |
QMap, QMultiMap | QMapIterator | QMutableMapIterator |
QHash, QMultiHash | QHashIterator | QMutableHashIterator |
在本讨论中,我们将集中讨论 QList 和 QMap。QLinkedList、QVector 和 QSet 的迭代器类型与 QList 的迭代器具有完全相同的接口;类似地,QHash 的迭代器类型具有与 QMap 的迭代器相同的接口。
与 STL 风格迭代器不同(下文将介绍),Java 风格迭代器指向项之间而不是直接指向项。因此,它们要么指向容器的开头(第一个项之前),要么指向容器的末尾(最后一个项之后),要么位于两个项之间。下面的图示展示了包含四个项的列表的有效迭代器位置,以红色箭头表示:
下面是一个典型的循环,用于按顺序迭代 QList 中的所有元素,并将它们打印到控制台上:
QList<QString> list;
list << "A" << "B" << "C" << "D";
QListIterator<QString> i(list);
while (i.hasNext())
QString s = i.next();
其工作原理如下:要迭代的 QList 被传递给 QListIterator 构造函数。此时,迭代器位于列表中第一项(“A” 项)之前。然后我们调用 hasNext() 来检查迭代器后面是否有项。如果有,我们调用 next() 来跳过该项。next() 函数返回它跳过的项。对于 QList,该项的类型是 QString。
以下是如何在 QList 中向后迭代的方法:
QListIterator<QString> i(list);
i.toBack();
while (i.hasPrevious())
QString s = i.previous();
这段代码与向前迭代的对称,只是我们首先调用 toBack() 将迭代器移动到列表中最后一项之后。
下面的图示说明了在迭代器上调用 next() 和 previous() 的效果:
下表总结了 QListIterator 的 API:
函数 | 行为 |
---|---|
toFront() | 将迭代器移到列表的前面(第一项之前) |
toBack() | 将迭代器移到列表的后面(最后一项之后) |
hasNext() | 如果迭代器不在列表的末尾,则返回 true |
next() | 返回下一项并将迭代器向前移动一个位置 |
peekNext() | 返回下一项但不移动迭代器 |
hasPrevious() | 如果迭代器不在列表的开头,则返回 true |
previous() | 返回前一项并将迭代器向后移动一个位置 |
peekPrevious() | 返回前一项但不移动迭代器 |
QListIterator 不提供用于在迭代时插入或删除列表项的函数。为了实现这一点,您必须使用 QMutableListIterator。以下是一个示例,我们使用 QMutableListIterator 从 QList 中删除所有奇数:
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() % 2 != 0)
i.remove();
}
在循环中的 next() 调用每次都会执行。它会跳过列表中的下一项。remove() 函数从列表中移除我们跳过的最后一项。对 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() 函数返回列表中项的非 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” 结尾的 (首都,国家) 对:
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风格迭代器
自Qt 2.0发布以来,STL风格迭代器就已经可用了。它们与Qt和STL的通用算法兼容,并且经过了速度优化。
对于每个容器类,都有两种STL风格的迭代器类型:一种提供只读访问,另一种提供读写访问。应尽可能使用只读迭代器,因为它们比读写迭代器更快。
容器 | 只读迭代器 | 读写迭代器 |
---|---|---|
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 |
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 *
的typedef,并且const_iterator类型只是const T *
的typedef。
在本讨论中,我们将集中讨论QList和QMap。QLinkedList,QVector和QSet的迭代器类型具有与QList的迭代器相同的接口;同样,QHash的迭代器类型具有与QMap的迭代器相同的接口。
以下是按顺序遍历QList的所有元素并将它们转换为小写的典型循环示例:
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(),因此我们永远不会执行循环。
下图显示了一个包含四个项目的向量的有效迭代器位置,以红色箭头表示:
使用STL风格迭代器进行反向迭代是通过反向迭代器完成的:
QList<QString> list;
list << "A" << "B" << "C" << "D";
QList<QString>::reverse_iterator i;
for (i = list.rbegin(); i != list.rend(); ++i)
*i = i->toLower();
}
到目前为止的代码片段中,我们使用一元*
操作符来检索存储在特定迭代器位置的项目(类型为QString),然后对其调用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。如果要使用STL迭代器对其进行迭代,您应始终复制容器并对副本进行迭代。例如:
// 正确
const QList<int> sizes = splitter->sizes();
QList<int>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
...
// 错误
QList<int>::const_iterator i;
for (i = splitter->sizes().begin();
i != splitter->sizes().end(); ++i)
...
对于返回对容器的const或非const引用的函数,不会出现此问题。
隐式共享迭代器问题
隐式共享对STL风格的迭代器还有另一个影响:在容器上的迭代器处于活动状态时,应避免复制容器。迭代器指向内部结构,如果复制容器,则应非常小心迭代器。例如:
QVector<int> a, b;
a.resize(100000); // 生成一个填充了0的大向量。
QVector<int>::iterator i = a.begin();
// 使用迭代器 i 的错误方式:
b = a;
/*
现在我们应该小心迭代器 i,因为它将指向共享数据
如果我们执行 *i = 4,那么我们会改变共享实例(两个向量)
这种行为与STL容器不同。在Qt中避免做这样的事情。
*/
a[0] = 5;
/*
容器 a 现在与共享数据分离,
即使 i 是来自容器 a 的迭代器,它现在也作为容器 b 中的迭代器工作。
在这里的情况是 (*i) == 0。
*/
b.clear(); // 现在迭代器 i 完全无效。
int j = *i; // 未定义的行为!
/*
来自 b 的数据(i 所指向的数据)已经消失。
这在STL容器中是明确定义的(*i == 5),
但对于 QVector 来说,这可能会导致崩溃。
*/
上面的示例只显示了 QVector 中的问题,但这个问题存在于所有隐式共享的Qt容器中。
foreach 关键字
如果你只想按顺序迭代容器中的所有项目,你可以使用 Qt 的 foreach
关键字。该关键字是 C++ 语言的 Qt 特定扩展,使用预处理器实现。
它的语法是:foreach
(变量, 容器) 语句。例如,这是如何使用 foreach
遍历QLinkedList的示例:
QLinkedList<QString> list;
...
QString str;
foreach (str, list)
qDebug() << str;
与使用迭代器的等效代码相比,foreach
代码显着更短:
QLinkedList<QString> list;
...
QLinkedListIterator<QString> i(list);
while (i.hasNext()) {
QString s = i.next();
qDebug() << s;
}
除非数据类型包含逗号(例如,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
自动访问(键,值)对的值组件,因此你不应该在容器上调用 values()(它会生成不必要的副本,见下文)。如果你想遍历键和值,你可以使用迭代器(更快),或者你可以获取键,并使用它们获取值:
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
循环时自动对容器进行复制。如果在迭代时修改容器,那么不会影响循环。(如果你不修改容器,则仍然会进行复制,但由于隐式共享,复制容器非常快。)
由于 foreach
创建容器的副本,对于变量使用非 const 引用并不允许修改原始容器。这只影响副本,这可能不是你想要的。
Qt 的 foreach
循环的替代方案是 C++ 11 及更新版本中的范围-based for
。然而,要注意范围-based for
可能会强制 Qt 容器进行分离,而 foreach
不会。但是使用 foreach
总是会复制容器,这对于 STL 容器来说通常不便宜。如果不确定,应优先选择 Qt 容器的 foreach
,并选择 STL 容器的范围-based for
。
除了 foreach
,Qt 还提供了一个用于无限循环的 forever
伪关键字:
forever {
...
}
如果你担心命名空间污染,你可以通过将以下行添加到你的 .pro
文件来禁用这些宏:
CONFIG += no_keywords
其他类似容器的类
Qt 包括其他一些类模板,在某些方面类似于容器。这些类不提供迭代器,也不能与 foreach
关键字一起使用。
- QCache<Key, T> 提供了一个缓存,用于存储与键类型 Key 关联的某种类型 T 的对象。
- QContiguousCache 提供了一种高效的方式来缓存通常以连续方式访问的数据。
- QPair<T1, T2> 存储一对元素。
与 Qt 的模板容器竞争的其他非模板类型包括 QBitArray, QByteArray, QString 和 QStringList。
算法复杂度
算法复杂度关注每个函数在容器中的项目数量增长时的运行速度(快或慢)。例如,在 QLinkedList 中插入一个项目是一个极快的操作,不管存储在 QLinkedList 中的项目数量是多少。另一方面,在 QVector 中间插入一个项目可能非常昂贵,如果 QVector 包含许多项目,则必须将一半的项目在内存中移动一个位置。
为了描述算法复杂度,我们使用以下术语,基于“大 O”符号:
- 常数时间: O(1)。如果一个函数需要的时间与容器中的项目数量无关,那么该函数被称为在常数时间内运行。一个例子是 QLinkedList::insert()。
- 对数时间: O(log n)。在对数时间内运行的函数是一种其运行时间与容器中的项目数量的对数成正比的函数。一个例子是二分查找算法。
- 线性时间: O(n)。在线性时间内运行的函数将以与容器中存储的项目数量直接成比例的时间执行。一个例子是 QVector::insert()。
- 线性对数时间: O(n log n)。在线性对数时间内运行的函数比线性时间函数慢,但比二次时间函数快。
- 二次时间: O(n²)。二次时间函数执行的时间与容器中存储的项目数量的平方成正比。
以下表格总结了 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) |
在表格中,“Amort.” 代表“摊销行为”。例如,“Amort. O(1)” 意味着如果您只调用一次该函数,您可能会获得 O(n) 的行为,但如果您多次调用它(例如 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 | Amort. O(1) | O(n) | Amort. O(1) | O(n) |
对于 QVector、QHash 和 QSet,追加项目的性能是摊销 O(log n)。可以通过在插入项目之前使用期望的项目数量调用 QVector::reserve()、QHash::reserve() 或 QSet::reserve() 来将其降低到 O(1)。下一节将更深入地讨论这个主题。
扩容策略
QVector、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;
}
我们通过每次附加一个字符来动态构建字符串 out。假设我们向 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 个被占用。
上面的值可能看起来有点奇怪,但这里有一些指导原则:
- QString 每次分配 4 个字符的空间,直到达到大小 20。
- 从 20 到 4084,它每次加倍大小。更确切地说,它前进到下一个二次幂,减去 12。 (一些内存分配器在请求确切的二次幂时执行更差,因为它们对每个块使用几个字节用于记账。)
- 从 4084 开始,它每次增加 2048 个字符(4096 字节)的块。这是有道理的,因为现代操作系统在重新分配缓冲区时不会复制整个数据;物理内存页仅仅被重新排序,并且只有第一页和最后一页的数据实际上需要被复制。
QByteArray 和 QList 使用与 QString 大致相同的算法。
QVector 对于可以使用 memcpy() 在内存中移动的数据类型(包括基本的 C++ 类型、指针类型和 Qt 的共享类)也使用该算法,但对于只能通过调用复制构造函数和析构函数移动的数据类型,它使用不同的算法。由于在这种情况下重新分配的成本更高,QVector 在空间不足时总是将内存大小加倍,从而减少重新分配的次数。
QHash<Key, T> 是一个完全不同的情况。QHash 的内部哈希表按二次幂增长,每次增长时,项目被重新定位到一个新的桶中,计算方法是 qHash(key) % QHash::capacity()(桶的数量)。这个观点也适用于 QSet 和 QCache<Key, T>。
对于大多数应用程序,Qt 提供的默认扩容算法就足够了。如果你需要更多的控制,QVector、QHash<Key, T>、QSet、QString 和 QByteArray 提供了一组函数,允许你检查和指定存储项目所需的内存量:
- capacity() 返回分配内存的项目数量(对于 QHash 和 QSet,哈希表中的桶数)。
- reserve(size) 显式地为 size 个项目预先分配内存。
- squeeze() 释放不需要存