文章目录
1.Qt Container
1.1. 介绍
Qt库提供了一组基于通用模板的容器类。这些类可用于存储指定类型的项目。例如,如果需要可调整大小的QString数组,请使用QVector。
这些容器类的设计比STL容器更轻,更安全且更易于使用。如果您不熟悉STL,或者更喜欢以“Qt方式”进行操作,则可以使用这些类而不是STL类。
容器类是隐式共享的,它们是可重入的,并且已针对速度,低内存消耗和最小的内联代码扩展进行了优化,从而生成了较小的可执行文件。此外,在所有用于访问它们的线程都将它们用作只读容器的情况下,它们是线程安全的。
要遍历存储在容器中的项目,可以使用两种类型的迭代器之一:Java样式的迭代器和STL样式的迭代器。Java样式的迭代器更易于使用并提供高级功能,而STL样式的迭代器效率更高,并且可以与Qt和STL的通用算法一起使用。
Qt还提供了一个foreach关键字,可以很容易地遍历容器中存储的所有项目。
**注意:**从Qt5.14开始,范围构造器可用于大多数容器类。QMultiMap是一个明显的例外。鼓励使用它们代替各种from/to方法。例如:
QVector<int>vector{1,2,3,4,4,5};
QSet<int>set(vector.begin(),vector.end());
/*
WillgenerateaQSetcontaining1,2,4,5.
*/
1.2. 具体容器类
1.2.1 顺序容器:
-
QList
-
QLinkedList
-
QVector
-
QStack
-
QQueue。
对于大多数应用程序,QList是最佳使用类型。尽管它是作为数组列表实现的,但它提供了非常快的前置和追加操作。如果您确实需要一个链表,请使用QLinkedList。This class is obsolete.
如果希望项目占据连续的内存位置,请使用QVector。
QStack和QQueue是提供LIFO和FIFO语义的便捷类。
1.2.2 关联容器:
- QMap
- QMultiMap
- QHash
- QMultiHash
- QSet
Multi容器方便地支持与单个键关联的多个值。
Hash容器通过使用哈希函数而不是对排序集进行二进制搜索来提供更快的查找。
1.2.3 缓存存储:
- QCache
- QContiguousCache
这两个类在有限的缓存存储中提供了对象的有效哈希查找。
Class | 摘要 |
---|---|
QList | 最常用的容器类。存储可以通过索引访问的给定类型(T)的值的列表。在内部,QList是使用数组实现的,从而确保基于索引的访问非常快。使用QList::append()和QList::prepend()将项目添加到列表的任一端使用QList::insert()将它们插入中间。与其他任何容器类相比,QList经过了高度优化,可以在可执行文件中扩展为尽可能少的代码。 |
QLinkedList | 类似QList,不同之处在于它使用迭代器而不是整数索引来访问项目。当插入大列表中间时,它还提供比QList更好的性能,并且具有更好的迭代器语义。(指向QLinkedList中某个项目的迭代器只要该项目存在就保持有效,而QList的迭代器在任何插入或删除后都可能变为无效。) |
QVector | 将给定类型的值的数组存储在内存中的相邻位置。在向量的前面或中间插入可能会非常慢,因为这可能导致大量项目必须在内存中移动一个位置。 |
QVarLengthArray<T,Prealloc> | 这提供了一个低级的可变长度数组。在速度特别重要的地方,可以使用它代替QVector。 |
QStack | 这是QVector的便利子类,它提供“后进先出”(LIFO)语义。它为QVector中已经存在的功能添加了以下功能:push(),pop()和top()。 |
QQueue | 这是QList的便利子类,它提供“先进先出”(FIFO)语义。它为QList中已经存在的功能添加以下功能: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*>。
1.2.4 示例:
满足要求的自定义数据类型:
class **Employee**
{
public:
Employee(){}
Employee(constEmployee&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。
1.3. 迭代器类
迭代器提供了访问容器中项目的统一方法。Qt的容器类提供两种类型的迭代器:Java样式的迭代器和STL样式的迭代器。当容器中的数据由于对非常量成员函数的调用而被修改或从隐式共享副本中分离时,两种类型的迭代器均无效。
1.3.1. Java样式迭代器
Java样式的迭代器是Qt4中的新增功能,并且是Qt应用程序中使用的标准迭代器。它们比STL样式的迭代器更方便使用,但代价是效率略低。他们的API以Java的迭代器类为模型。
对于每个容器类,有两种Java样式的迭代器数据类型:一种提供只读访问,另一种提供读写访问。
容器 | 只读迭代器 | 读写迭代器 |
---|---|---|
QList,QQueue | QListIterator | QMutableListIterator |
QLinkedList | QLinkedListIterator | QMutableLinkedListIterator |
QVector,QStack | QVectorIterator | QMutableVectorIterator |
QSet | QSetIterator | QMutableSetIterator |
QMap<Key,T>,QMultiMap<Key,T> | QMapIterator<Key,T> | QMutableMapIterator<Key,T> |
QHash<Key,T>,QMultiHash<Key,T> | QHashIterator<Key,T> | QMutableHashIterator<Key,T> |
在此讨论中,我们将专注于QList和QMap。QLinkedList,QVector和QSet的迭代器类型与QList的迭代器具有完全相同的接口。同样,QHash的迭代器类型与QMap的迭代器具有相同的接口。
与STL样式的迭代器(如下所述)不同,Java样式的迭代器在项目之间指向而不是直接指向项目。因此,它们要么指向容器的最开始(在第一个项目之前),要么指向容器的最末端(在最后一个项目之后),或者指向两个项目之间。
下图包含四个项目的列表中的红色箭头为有效的迭代器位置:
这是一个典型的循环,用于依次遍历QList的所有元素并将其打印到控制台:
它的工作方式如下:将要迭代的QList传递给QListIterator构造函数。此时,迭代器位于列表中第一个项目的前面(项目“A”之前)。然后我们调用hasNext()来检查迭代器之后是否有一个项目。如果存在,我们调用next()跳过该项目。next()函数返回它跳过的项目。对于QList,该项目的类型为QString。
#include <QtCore>
int main()
{
QList<QString> list;
list << "A" << "B" << "C" << "D";
QListIterator<QString> i(list);
while(i.hasNext() )
qDebug() << i.next();
}
这是在QList中向后迭代的方法:
QListIterator<QString> i(list);
i.toBack();
while(i.hasPrevious())
qDebug()<<i.previous();
该代码是向前迭代对称的,除了我们从调用toBack()开始,以将迭代器移动到列表中的最后一项之后。
下图说明了在迭代器上调用next()和previous()的效果:
下表总结了QListIteratorAPI:
Function | 行为 |
---|---|
toFront() | 将迭代器移到列表的开头(在第一项之前) |
toBack() | 将迭代器移到列表的末尾(最后一项之后) |
hasNext() | 如果迭代器不在列表的后面,则返回true |
next() | 返回下一项并将迭代器前进一个位置 |
peekNext() | 返回下一项而不移动迭代器 |
hasPrevious() | 如果迭代器不在列表的最前面,则返回true |
previous() | 返回上一项,并将迭代器后退一个位置 |
peekPrevious() | 返回上一项而不移动迭代器 |
QListIterator不提供在迭代时从列表中插入或删除项目的功能。为此,必须使用QMutableListIterator。这是一个示例,其中我们使用QMutableListIterator从QList中删除所有奇数:
#include <QtCore>
int main ()
{
QList<int> list;
list << 1 << 2 << 3 << 4 << 5 << 6;
qDebug()<<list;
QMutableListIterator<int> i(list);
while(i.hasNext())
{
if (i.next()%2!=0)
i.remove();
}
qDebug() << list;
}
每次都会在循环中进行next()调用。它将跳过列表中的下一项。remove()函数从列表中删除我们跳过的最后一项。调用remove()不会使迭代器无效,因此可以安全地继续使用它。向后迭代时,效果也一样:
QMutableListIterator<int> i(list);
i.toBack();
while( i.hasPrevious() )
{
if( i.previous() %2 !=0 )
i.remove();
}
qDebug()<<list;
如果我们只想修改现有项目的值,则可以使用setValue()。在下面的代码中,我们将大于128的任何值替换为128:
#include <QtCore>
int main()
{
QList<int>list;
list<<10<<50<<150<<200<<250<<300;
qDebug()<<list;
QMutableListIterator<int>i(*list*);
while( i.hasNext() )
{
if(i.next()>128)
i.setValue(128);
}
qDebug()<<list;
}
就像remove()一样,setValue()对我们跳过的最后一项进行操作。如果我们向前迭代,这就是迭代器之前的项目;如果我们向后迭代,则这是迭代器之后的项目。
next()函数返回对列表中项目的非常量引用。对于简单的操作,我们甚至不需要setValue():
QMutableListIterator<int>i(*list*);
while( i.hasNext() )
i.next() *= 2;
qDebug()<<list;
如上所述,QLinkedList,QVector和QSet的迭代器类具有与QList完全相同的API。现在,我们转向QMapIterator,它有所不同,因为它在(键,值)对上进行迭代。
与QListIterator一样,QMapIterator提供toFront(),toBack(),hasNext(),next(),peekNext(),hasPrevious(),previous()和peekPrevious()。通过在next(),peekNext(),previous()或peekPrevious()返回的对象上调用key()和value()来提取键和值分量。
下面的示例删除所有首都名称以“City”结尾的(capital,country)对:
#include <QtCore>
int main ( )
{
QMap<QString,QString>map;
map.insert("Paris","France");
map.insert("GuatemalaCity","Guatemala");
map.insert("MexicoCity","Mexico");
map.insert("Moscow","Russia");
qDebug() << map;
QMutableMapIterator<QString,QString>i( map );
while(i.hasNext())
{
if(i.next().key().endsWith("City"))
i.remove();
}
qDebug() << map;
}
QMapIterator还提供了直接在迭代器上运行的key()和value()函数,并返回迭代器跳转到其上的最后一项的键和值。例如,以下代码将QMap的内容复制到QHash中:
int main ()
{
QMap<QString,QString> map;
map.insert("Paris","France");
map.insert("GuatemalaCity","Guatemala");
map.insert("MexicoCity","Mexico");
map.insert("Moscow","Russia");
qDebug() << map;
QHash < QString,QString>hash;
QMapIterator < QString,QString > i(map);
while( i.hasNext() )
{
i.next();
hash.insert( i.key(), i.value() );
}
qDebug() << hash;
}
如果要遍历具有相同值的所有项目,则可以使用findNext()或findPrevious()。这是一个示例,其中我们删除了具有特定值的所有项目:
int main ()
{
QMap< QString,QString >map;
map.insert("Paris","France");
map.insert("GuatemalaCity","Guatemala");
map.insert("MexicoCity","Mexico");
map.insert("Moscow","Russia");
map.insert("MO?","Russia");
qDebug() << map;
QMutableMapIterator< QString, QString > i ( map );
while( i.findNext("Russia") )
i.remove();
qDebug() << map;
}
1.3.2. STL样式迭代器
自Qt2.0发行以来,已经可以使用STL样式的迭代器。它们与Qt和STL的通用算法兼容,并针对速度进行了优化。
对于每个容器类,有两种STL样式的迭代器类型:一种提供只读访问,另一种提供读写访问。应尽可能使用只读迭代器,因为它们比读写迭代器快。
容器 | 只读迭代器 | 读写迭代器 |
---|---|---|
QList,QQueue | QList::const_iterator | QList::iterator |
QLinkedList | QLinkedList::const_iterator | QLinkedList::iterator |
QVector,Stack | 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类型仅为constT*的typedef。
在此讨论中,我们将专注于QList和QMap。QLinkedList,QVector和QSet的迭代器类型与QList的迭代器具有完全相同的接口。同样,QHash的迭代器类型与QMap的迭代器具有相同的接口。
这是一个典型的循环,用于依次遍历QList的所有元素并将其转换为小写:
int main ()
{
QList< QString > list;
list << "A" << "B" << "C" << "D";
qDebug() << list;
QList< QString >::iterator i;
for( i=list.begin(); i!=list.end(); ++i)
*i = (*i).toLower();
qDebug() << list;
}
与Java样式的迭代器不同,STL样式的迭代器直接指向项目。容器的begin()函数返回一个迭代器,该迭代器指向容器中的第一项。容器的end()函数将迭代器返回到虚数项,该虚项在容器中最后一项之后。end()标记无效位置;绝对不能取消引用。它通常用于循环的中断条件中。如果列表为空,则begin()等于end(),因此我们从不执行循环。
下图将有效迭代器位置显示为包含四个项目的矢量的红色箭头:
使用STL样式的迭代器反向迭代是通过反向迭代器完成的:
int *main ()
{
QList< QString >list;
list << "A" << "B" << "C" << "D";
QList< QString >::reverse_iterator i;
for( i=list.rbegin(); i!=list.rend(); ++i)
{
*i = i->toLower();
qDebug() << *i;
}
}
到目前为止,在代码片段中,我们使用一元*运算符检索存储在某个迭代器位置的QString类型的项目,然后在其上调用QString::toLower()。大多数C++编译器还允许我们编写i->toLower(),但有些则不允许。
对于只读访问,可以使用const_iterator,constBegin()和constEnd()。例如:
int main *()
{
QList< QString > list;
list<<"A"<<"B"<<"C"<<"D";
QList< QString >::const_iteratori;
for( i = list.constBegin(); i != list.constEnd(); ++i )
qDebug() << *i;
}
下表总结了STL样式的迭代器的API:
Expression | 行为 |
---|---|
*i | 返回当前项目 |
++i | 将迭代器前进到下一个项目 |
i+=n | 使迭代器前进n个项目 |
–i | 将迭代器后退一个项目 |
i-=n | 将迭代器后退n个项目 |
i-j | 返回迭代器i和j之间的项目数 |
++和-运算符可用作前缀(++i,–i)和后缀(i++,i–)运算符。前缀版本会修改迭代器,并返回对修改后的迭代器的引用;后缀版本在修改迭代器之前先获取它的副本,然后返回该副本。在忽略返回值的表达式中,我们建议您使用前缀运算符(++i,–i),因为它们稍快一些。
对于非常量迭代器类型,可以在赋值运算符的左侧使用一元*运算符的返回值。
对于QMap和QHash,*运算符返回项目的值组件。如果要检索密钥,请在迭代器上调用key()。为了对称起见,迭代器类型还提供了value()函数来检索该值。例如,这是我们将QMap中的所有项目打印到控制台的方法:
int main ()
{
QMap< QString,QString > map;
map.insert("Paris", "France");
map.insert("GuatemalaCity", "Guatemala");
map.insert("MexicoCity", "Mexico");
map.insert("Moscow", "Russia");
QMap < QString, QString >::const_iterator i;
for( i = map.constBegin(); i != map.constEnd(); ++i)
qDebug() << i.key() << ':' << i.value();
}
由于隐式共享,函数为每个值返回一个容器非常便宜。QtAPI包含许多函数,这些函数返回一个QList或QStringList(例如,QSplitter::sizes())。如果要使用STL迭代器对其进行迭代,则应始终获取容器的副本并对该副本进行迭代。例如:
//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)
...
对于将const或非const引用返回到容器的函数,不会发生此问题。
1.3.3. 隐式共享迭代器的问题
隐式共享在STL样式的迭代器上还有另一个后果:当迭代器在该容器上处于活动状态时,应避免复制该容器。迭代器指向内部结构,如果您复制容器,则应非常小心使用迭代器。例如:
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;
/*
现在我们应该对迭代器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容器都存在该问题。
1.4. foreach 关键字
如果只想按顺序遍历容器中的所有项目,则可以使用Qt的foreach关键字。关键字是Qt特定于C++语言的添加,并使用预处理器实现。
它的语法是:foreach( 变量,容器 )语句。例如,以下是使用foreach遍历 QLinkedList 的方法:
int main ()
{
QLinkedList<QString>list;
list.append("one");
list.append("two");
list.append("three");
QStringstr;
foreach(str,list)
qDebug()<<str;
}
foreach代码明显短于使用迭代器的等效代码:
QLinkedList<QString> list;
...
QLinkedListIterator < QString > i(list);
while( i.hasNext() )
qDebug() << i.next();
除非数据类型包含逗号(例如QPair<int,int>),否则可以在foreach语句中定义用于迭代的变量:
QLinkedList< QString > list;
...
foreach( const QString & str, list)
qDebug() << str;
与其他任何C++循环构造一样,您可以在foreach循环的主体周围使用花括号,并且可以使用break离开循环:
QLinkedList < QString >list;
...
foreach(constQString & 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会创建容器的副本,因此对变量使用非常量引用将不允许您修改原始容器。它只会影响副本,可能不是您想要的。
Qt的foreach循环的替代方法是基于范围的,它是C++11和更高版本的一部分。但是,请记住,基于范围的for可能会强制Qt容器分离,而foreach则不会。但是使用foreach总是会复制容器,这对于STL容器通常并不便宜。如有疑问,对于Qt容器,首选foreach,对于STL容器,首选基于范围。
除了foreach之外,Qt还为无限循环提供了永远的伪关键字:
forever{...}
如果您担心命名空间污染,可以通过在.pro文件中添加以下行来禁用这些宏:
CONFIG += no_keywords
1.5. 其他类似容器的类
-
Qt 包括在某些方面类似于容器的其他模板类,这些类不提供迭代器,因此不能与 foreach 关键字一起使用。
-
QCache<Key,T>提供了一个缓存,用于存储与Key类型的键相关联的T类型的对象。
-
QContiguousCache提供了一种缓存数据的有效方法,该数据通常以连续的方式访问。
-
QPair<T1,T2>存储一对元素。
-
-
与Qt的模板容器竞争的其他非模板类型
- QBitArray
- QByteArray
- QString 和 QStringList。
1.6. 算法的复杂性
算法复杂度关系到每个功能随着容器中项目数量的增长有多快(或慢)。例如,无论QLinkedList中存储的项目数如何,在QLinkedList的中间插入项目都是非常快速的操作。另一方面,如果QVector包含许多项目,则在QVector的中间插入项目可能非常昂贵,因为一半项目必须在内存中移动一个位置。
为了描述算法的复杂性,我们基于“bigOh”表示法使用以下术语:
- 恒定时间:O(1)。 如果某个功能需要相同的时间量,则无论容器中有多少个物品,该功能都将在恒定时间内运行。一个示例是QLinkedList::insert()。
- 对数时间:O(logn)。以对数时间运行的函数是其运行时间与容器中项目数的对数成正比的函数。一个例子是二进制搜索算法。
- 线性时间:O(n)。以线性时间运行的函数将在与容器中存储的项目数量成正比的时间内执行。一个示例是QVector::insert()。
- 线性对数时间:O(nlogn)。在线性对数时间中运行的函数在渐近上比线性时间函数慢,但比二次时间函数快。
- 二次时间:O(n²)。二次时间函数的执行时间与容器中存储的项目数的平方成正比。
下表总结了Qt的顺序容器类的算法复杂性:
Indexlookup | Insertion | Prepending | Appending | |
---|---|---|---|---|
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关联容器和集合的算法复杂性:
Keylookup | Insertion | |||
---|---|---|---|---|
Average | Worstcase | Average | Worstcase | |
QMap<Key,T> | O(logn) | O(logn) | O(logn) | O(logn) |
QMultiMap<Key,T> | O(logn) | O(logn) | O(logn) | O(logn) |
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(logn)。
在插入项目之前,可以通过调用QVector::reserve(),QHash::reserve()或QSet::reserve()并使用预期的项目数将其降至O(1)。下一节将更深入地讨论该主题。
1.7. 成长策略
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;
}
我们通过一次附加一个字符来动态构建字符串。假设我们在QString字符串后附加15000个字符。然后,当QString空间不足时,会发生以下18个重新分配(可能的15000个):4,8,12,16,16,20,52,116,244,500,1012,2036,4084,6132,8180,10228,12276、14324、16372。最后,QString分配了16372个Unicode字符,其中15000个被占用。
上面的值可能看起来有些奇怪,但是这里是指导原则:
-
QString一次分配4个字符,直到达到大小20。
-
从20到4084,每次将大小增加一倍就可以提高。更准确地说,它前进到下一个2的幂,再减去12。(某些内存分配器在要求精确的2的幂时性能最差,因为它们在每个块中使用几个字节进行记账。)
-
从4084开始,它前进2048个字符(4096字节)的块。这是有道理的,因为现代操作系统在重新分配缓冲区时不会复制整个数据。只需简单地对物理内存页面进行重新排序,实际上只需要复制首页和最后一页上的数据。
QByteArray 和 QList使用与QString大致相同的算法。
QVector 还使用该算法处理可以使用memcpy()在内存中移动的数据类型(包括基本的C++类型,指针类型和Qt的共享类),但对只能使用通过调用复制构造函数和析构函数进行移动。由于在这种情况下重新分配的成本较高,因此QVector通过在空间不足时始终将内存翻倍来减少重新分配的次数。
QHash<Key,T>是完全不同的情况。QHash的内部哈希表以2的幂次方增长,并且每次增长时,这些项都将重新放置在新的存储桶中,计算方式为 qHash(key) % QHash::capacity() (存储桶数)。此注释也适用于QSet和QCache<Key,T>。
对于大多数应用程序,Qt提供的默认增长算法可以解决问题。如果需要更多控制,QVector,QHash<Key,T>,QSet,QString 和 QByteArray提供了三个函数,可让您检查并指定用于存储项目的内存量:
-
Capacity()返回为其分配内存的项目数(对于QHash和QSet,为哈希表中的存储桶数)。
-
reserve(size)显式预分配大小项目的内存。
-
squeeze()释放不需要存储项目的任何内存。
如果您知道将在一个容器中存储大约多少个项目,则可以通过调用reserve()开始,然后在完成容器的填充后,可以调用squeeze()释放额外的预分配内存。