Qt容器类

在Qt库中为我们提供了一系列的基于模板的容器类。这些类可以被用来存储特定类型的项。例如,如果你需要一个大小可以变得QString数组,那么可以使用QVector

这些容器类都是隐式共享的,可重入的,并且在速度上进行了优化,内存占用少,内联代码扩展少,从而可以产生更小的可执行文件。此外,当他们被用作只读容器时,还是线程安全的。对于遍历这些容器来说,可以使用两种类型的迭代器:Java风格的迭代器和STL风格的迭代器。其中,Java风格的迭代器更容易使用,特别是对于Java工作人员来说,它提供了高层次的函数;然而,STL风格的迭代器会更高效,并且可以和Qt和STL的通用算法结合使用。另外,Qt还提供了一个foreach关键字,使遍历容器中的每一项更容易了。

Qt中的容器和STL中的类似,也分为序列式容器和关联式容器。其中,序列式容器有:QList,QLinkedList,QVector,QStack,QQueue。对大部分应用程序来说,QList都是一个很好的选择。尽管它在底层被实现为一个array-list,但它为我们提供了非常快速的添加操作,包括在头部添加和在尾部添加。如果你确实需要一个linked-list,可以使用QLinkedList;如果你想确保你的元素占用连续的内存空间,可以使用QVector。而QStack和QQueue是两个提供了LIFO和FIFO语义的方便类。

除了序列式容器,Qt中还提供了关联式容器:QMap,QMultiMap,QHash,QMultiHash,QSet。这些容器中存储的都是key-value对。其中,"Multi"容器又支持一个key可以关联多个value。"Hash"容器通过使用一个hash函数而不是二分搜索提供了更快速的查找操作。

我们将这些容器类的总结在下表中:

QList 这是最通用的一个容器类。它里面存储了给定类型T的一个列表,这个列表可以使用下标来访问。其实,在底层QList被实现为一个数组,
确保基于下标的访问非常快速。可以使用QList::append()和QList::prepend()向链表的两端添加元素,或者使用QList::insert()在链表的中间插入元素。
并且,和其他容器相比,更重要的是,QList在可执行文件中展开的代码量是非常少的,是经过高度优化的。QStringList就继承自QList
QLinkedList 这个容器类类似于QList,只不过它是使用迭代器来访问,而不是下标。当从中间插入时,它的效率比QList还要高。并且,它有更好的迭代器语义。
即指向QLinkedList中某个元素的迭代器,只有该元素存在就会一直保持有效,而指向QList中某元素的迭代器,在向QList进行任意插入或删除时都会导致
该迭代器失效。
QVector 这个容器类会在一块相邻的内存中存储一个给定类型的值的数组。在一个vector的前端或中间插入是非常慢的,因为这会导致大量现存的元素移动以为新的
元素腾出位置。
QStack 这个容器类继承自QVector,提供了“先入后出”的语义。
QQueue 这个容器类继承自QList,提供了“先入先出”的语义。
QSet 这个容器类提供了不允许有重复值的集合,提供快速的查找效率。
QMap 这个容器类提供了一个字典形式的容器,它会将Key类型的值映射到T类型的value上。通常情况下,每一个key只关联一个值。并且,QMap会按Key的顺序存储
相应的值;所以,如果不关心元素的存储顺序,QHash是一个更好的选择。
QMaultiMap 这个容器类继承自QMap,提供了多值的字典,也就是说,该容器中的一个key可以关联多个值。
QHash 这个容器类的API和QMap几乎一样,但它提供了更快速的查找操作。并且,该类会按任意的顺序存储值。
QMultiHash 这个容器类继承自QHash,提供了多值hash表。

容器是可以嵌套使用的。例如,可以使用QMap>这种类型,其key的类型是QString,值类型是QList

上面提到的这些容器分别被定义在各自的、名称和容器名一样的头文件中。例如,

这些容器中存储的值可以是任何能被赋值的数据类型,即该类型必须提供一个默认的构造函数、一个拷贝构造函数、一个赋值运算符。这样的数据类型涵盖了大部分你可以存储的类型,包括基本类型入int和double,指针类型,Qt的数据类型QString,QDate,QTime,但不包括QObject或其子类(QWidget,QDialog,QTimer等等)。如果你尝试构建一个QList类型的变量,编译器就会提示你QWidget类的拷贝构造函数和赋值操作符是被禁用的。如果你想存储这些类的对象,可以存储它们的指针类型,例如QList

一个可以存储在容器中的可赋值数据类型,类似于下面这个自定义类型:

[cpp]  view plain  copy
  1. class Employee  
  2. {  
  3. public:  
  4.     Employee() {}  
  5.     Employee(const Employee &other);  
  6.   
  7.     Employee &operator=(const Employee &other);  
  8.   
  9. private:  
  10.     QString myName;  
  11.     QDate myDateOfBirth;  
  12. };  

如果我们不提供一个拷贝构造函数或赋值运算符,C++提供的默认实现是逐成员拷贝。在上面的例子中,这种默认构造函数也是足够的。同样,如果你没有提供构造函数,C++为我们提供的默认构造函数会使用成员变量所对应数据类型的默认值进行各个成员的初始化。所以,下面这种自定义数据类型虽然没有提供显式的构造函数或赋值运算符,它也可以被存储到容器中:

[cpp]  view plain  copy
  1. struct Movie  
  2. {  
  3.     int id;  
  4.     QString title;  
  5.     QDate releaseDate;  
  6. };  


而对于其他的容器可能还有特殊的要求。例如,QMap的Key类型必须提供operator<()。这些特定的要求在容器类的文档中都有详细说明。一般,如果某个要求没被满足,编译器就会报错。

Qt的容器还提供了operator<<() 和 operator>>() 以使它们可以方便的使用QDataStream类读取或写入数据。这也意味着存储在容器中的数据类型必须支持这两种操作。提供这种支持是很简单的。比如,下面的例子,是我们为上面声明的Movie结构体提供的 << 和 >>运算符:

[cpp]  view plain  copy
  1. QDataStream &operator<<(QDataStream &out, const Movie &movie)  
  2. {  
  3.     out << (quint32)movie.id << movie.title  
  4.         << movie.releaseDate;  
  5.     return out;  
  6. }  
  7.   
  8. QDataStream &operator>>(QDataStream &in, Movie &movie)  
  9. {  
  10.     quint32 id;  
  11.     QDate date;  
  12.   
  13.     in >> id >> movie.title >> date;  
  14.     movie.id = (int)id;  
  15.     movie.releaseDate = date;  
  16.     return in;  
  17. }  


大部分容器类的说明文档中都提到了“默认值”。例如,QVector会自动的使用默认构造函数的值初始化它的元素,QMap::value()方法在指定的key不存在的情况下会返回一个默认构造函数产生的值。对大部分数据类型来说,这只是意味着默认构造函数创建了一个值,例如,QString的默认构造函数会创建出一个空字符串。但是,对于int和double这类的基本类型,和指针类型,C++语言并不会指定任何初始化。在这种情况下,Qt的容器会自动的用0对它们进行初始化。

至于对容器的操作,和stl一样,通常是使用迭代器。迭代器为访问容器中的元素提供了一个统一的方式。Qt中的容器类提供了两类迭代器:Java风格的迭代器和STL风格的迭代器。但是,当容器中的数据被修改后或由于调用了non-const成员函数导致其脱离了隐式共享,那么这两种迭代器都会失效。

Java风格的迭代器:

Java风格的迭代器在Qt4中被引入,成功Qt应用程序的标准组件。它们比STL风格的迭代器更好用,带代价是效率更低。这些迭代器都是特定的类,所以其具体使用方法在每个类的文档中有详细说明。另外,每一个容器类都又提供了两种类型的Java风格的迭代器:一种是只读迭代器,一种是读写迭代器。详细类型说明如下表:

Containers Read-only iterator Read-write iterator
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风格的迭代器指向两个元素之间,而不是直接指向某个具体的元素。由于这个原因,这些迭代器要么指向第一个元素前面,要么指向最后一个元素后面,要么在某两个元素之间。下面的图示显示了对于一个链表来说,有效的迭代器指向:


下面的代码展示了,使用Java风格的迭代器遍历一个QList的典型做法:

[cpp]  view plain  copy
  1. QList list;  
  2. list << "A" << "B" << "C" << "D";  
  3.   
  4. QListIterator i(list);  
  5. while (i.hasNext())  
  6.     qDebug() << i.next();  

该代码的执行原理是:将要遍历的QList传给QListIterator的构造函数。此时,迭代器就指向了链表中第一个元素的前面,即"A"的前面。接着,我们调用hasNext()方法来判断在当前迭代器后面是否有一个元素。如果有,我们就调用next()函数来跳过那个元素。并且,next()函数会返回它跳过的那个元素。在这个例子中就是返回一个QString字符串。

下面的代码展示了怎么从后向前遍历一个QList:

[cpp]  view plain  copy
  1. QListIterator i(list);  
  2. i.toBack();  
  3. while (i.hasPrevious())  
  4.     qDebug() << i.previous();  

该代码和向前遍历的代码类似,除了我们在一开始调用了toBack()函数将迭代器移到最后一个元素的后面。

下面的图示说明了电影next() 和 previous()的作用:



下表中列出了QListIterator类的API及其作用:

toFront() 移动迭代器到第一个元素之前
toBack() 移动迭代器到最后一个元素之后
hasNext() 如果迭代器还未遍历到列表的最后,返回true
next() 返回下一个元素,并将迭代器向前移动一个位置。
peekNext() 返回下一个元素,不移动迭代器。
hasPrevious() 如果迭代器还未遍历到列表的前端,返回true。
previous() 返回前一个元素,并将迭代器向后移动一个位置。
peekPrevious() 返回前一个元素,不移动迭代器。

另外,上面我们就说过,QListIterator是只读迭代器,所以,我们无法使用该迭代器在遍历的过程中进行插入或删除操作。要使用这种功能,必须使用QMutableListIterator。下面的例子展示了使用QMutableListIterator来删除QList中所有的奇数:

[cpp]  view plain  copy
  1. QMutableListIterator<int> i(list);  
  2. while (i.hasNext()) {  
  3.     if (i.next() % 2 != 0)  
  4.         i.remove();  
  5. }  

在上面的循环中,每次循环都调用了next()函数。这会跳过列表中的下一个元素。remove()函数会从链表中删除我们之前跳过的那个元素。并且,remove()函数不会使迭代器失效,我们可以安全的继续使用它。对于,从后向前遍历也是一样,如下代码所示:

[cpp]  view plain  copy
  1. QMutableListIterator<int> i(list);  
  2. i.toBack();  
  3. while (i.hasPrevious()) {  
  4.     if (i.previous() % 2 != 0)  
  5.         i.remove();  
  6. }  

如果我们只是想修改一个现存的元素,我们可以使用setValue()函数。如下面的代码,我们用128替换容器中大于128的元素:

[cpp]  view plain  copy
  1. QMutableListIterator<int> i(list);  
  2. while (i.hasNext()) {  
  3.     if (i.next() > 128)  
  4.         i.setValue(128);  
  5. }  
类似于remove()函数,setValue()也是工作在我们刚跳过的元素上。如果我们是向前遍历,该元素就是当前迭代器之前的那个元素;如果我们是向后遍历,该元素就是当前迭代器之后的那个元素。

其实,next()函数会返回一个元素的非常量引用。所以,对应简单的操作,我们不需要调用setValue()函数,而是直接进行相应修改即可。如下代码:

[cpp]  view plain  copy
  1. QMutableListIterator<int> i(list);  
  2. while (i.hasNext())  
  3.     i.next() *= 2;  

我们上面提到过,QLinkedList,QVector,QSet的迭代器操作和QList完全一下。那么,下面我们就来看一下QMapIterator,因为该迭代器是工作在key-value对上,所以和上面讲的有点不同。

类似于QListIterator,QMapIterator也提供了toFront(),toBack(),hasNext(),next(),peekNext(),hasPrevious(),peekPrevious()。至于具体的key和value,我们可以调用key() 和 value() 函数,从next(),peekNext(),previous()或者peekPrevious()返回的对象中提取。

下面的例子中,我们从map中删除所有capital以"City" 结尾的(capital,country)对:

[cpp]  view plain  copy
  1. QMap map;  
  2. map.insert("Paris""France");  
  3. map.insert("Guatemala City""Guatemala");  
  4. map.insert("Mexico City""Mexico");  
  5. map.insert("Moscow""Russia");  
  6. ...  
  7.   
  8. QMutableMapIterator i(map);  
  9. while (i.hasNext()) {  
  10.     if (i.next().key().endsWith("City"))  
  11.         i.remove();  
  12. }  
其实,QMapIterator也提供了相应的key() 和 value() 函数,可以直接作用于迭代器本身,返回上次跳过的元素的键和值。例如,下面的代码将QMap的元素拷贝到QHash:

[cpp]  view plain  copy
  1. QMap<int, QWidget *> map;  
  2. QHash<int, QWidget *> hash;  
  3.   
  4. QMapIterator<int, QWidget *> i(map);  
  5. while (i.hasNext()) {  
  6.     i.next();  
  7.     hash.insert(i.key(), i.value());  
  8. }  
如果你想迭代所有具有特定值的元素,可以使用findNext()或findPrevious()。在下面的例子中,我们从容器中删除具有特定值的所有项:

[cpp]  view plain  copy
  1. QMutableMapIterator<int, QWidget *> i(map);  
  2. while (i.findNext(widget))  
  3.     i.remove();  


STL风格的迭代器:

STL风格的迭代器,在Qt 2 中就存在了。它们兼容于Qt和STL的通用算法,并且在访问速度上进行了优化。

同样,每一种容器也都提供了两种类型的STL风格迭代器:只读迭代器和读写迭代器。我们应尽量使用只读迭代器,因为它们更快。

我们同样用一张表来列举每一种STL风格的迭代器:

Containers Read-only iterator Read-write 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
QMap, QMultiMap QMap::const_iterator QMap::iterator
QHash, QMultiHash QHash::const_iterator QHash::iterator

STL迭代器的API在每一个类中也都有详细的说明。比如,++运算符会将迭代器前进到下一个元素,*运算符返回迭代器所指向的元素。事实上,对QVector和QStack来说,由于它们的元素都是存储在连续的内存中,所以它们的迭代器类型就是T*,它们的只读迭代器类型就是const T*。下面,我们还是以QList和QMap为例还说明stl风格的迭代器的使用方法。

下面的例子代码,是使用STL风格的迭代器遍历QList的典型方式:

[cpp]  view plain  copy
  1. QList list;  
  2. list << "A" << "B" << "C" << "D";  
  3.   
  4. QList::iterator i;  
  5. for (i = list.begin(); i != list.end(); ++i)  
  6.     *i = (*i).toLower();  
不同于Java风格的迭代器,STL风格的迭代器直接指向具体的元素。容器的begin() 方法返回一个指向容器中第一个元素的迭代器。end() 方法返回一个指向容器中最后一个元素的下一个位置的迭代器。end()标识了一个无效的位置,绝不应该对它解引用。它经常被用来在一个循环中做为结束条件。如果链表为空,begin()就等于end()。下面的图示说明了在一个容器中对STL风格的迭代器来说,有效的迭代器位置:



同样,使用STL风格的迭代器做反向遍历的代码如下:

[cpp]  view plain  copy
  1. QList list;  
  2. list << "A" << "B" << "C" << "D";  
  3.   
  4. QList::reverse_iterator i;  
  5. for (i = list.rbegin(); i != list.rend(); ++i)  
  6.     *i = i->toLower();  
  7. }  
到目前为止,我们在代码中都是使用一元运算符*来提取某个迭代器位置的元素内容,然后调用QString;:toLower()。其实,大部分c++编译器还允许我们使用i->toLower()的形式。对于只读迭代器,可以使用const_iterator,例如:

[cpp]  view plain  copy
  1. QList::const_iterator i;  
  2. for (i = list.constBegin(); i != list.constEnd(); ++i)  
  3.     qDebug() << *i;  
接下来,我们也用一张表来总结一下stl风格的迭代器的相关操作:

*i 返回当前元素
++i 步进迭代器到下一个元素位置
i += n 将迭代器向前步进n个元素
--i 步进迭代器到前一个位置
i -= n 将迭代器向前步进n个位置
i - j 返回 迭代器 i 和 j之间的元素个数

对于++和--操作,既支持前++,也支持后++,--也一样。至于这两者的区别,我相信大家都能理解,在此就不解释了。

对于非const迭代器类型,*运算符返回的值可以被当做左值来使用。

对于QMap和QHash来说,*运算符返回一个元素的value部分。如果你想获得key,可以在迭代器上调用key() 方法。而处于对称性,迭代器还提供了value() 方法来获得value()值。例如,下面的代码说明了怎么打印出QMap中的所有元素:

[cpp]  view plain  copy
  1. QMap<intint> map;  
  2. ...  
  3. QMap<intint>::const_iterator i;  
  4. for (i = map.constBegin(); i != map.constEnd(); ++i)  
  5.     qDebug() << i.key() << ':' << i.value();  
并且,由于 “隐式共享”,一个函数返回一个容器的代价并不高。在Qt的API中,包含了很多返回QList或QStringList的函数,比如QSplitter::sizes()。如果你想使用STL风格的迭代器来迭代这些容器,你应该先拿到该容器的一份拷贝,然后遍历这份拷贝。例如:

[cpp]  view plain  copy
  1. // RIGHT  
  2. const QList<int> sizes = splitter->sizes();  
  3. QList<int>::const_iterator i;  
  4. for (i = sizes.begin(); i != sizes.end(); ++i)  
  5.     ...  
  6.   
  7. // WRONG  
  8. QList<int>::const_iterator i;  
  9. for (i = splitter->sizes().begin();  
  10.         i != splitter->sizes().end(); ++i)  
  11.     ...  


foreach 关键字

如果你只是想顺序的变量容器中的所以元素,可以使用Qt的foreach关键字。这个关键字是Qt特定的,是使用预处理器实现的。

它的语法是:foreach(variable, container) statement。例如,下面的代码说明了怎么使用foreach来迭代QLinkedList

[cpp]  view plain  copy
  1. QLinkedList list;  
  2. ...  
  3. QString str;  
  4. foreach (str, list)  
  5.     qDebug() << str;  
使用foreach的代码,通常都会比使用迭代器写出的代码更短:

[cpp]  view plain  copy
  1. QLinkedList list;  
  2. ...  
  3. QLinkedListIterator i(list);  
  4. while (i.hasNext())  
  5.     qDebug() << i.next();  
如果容器中的数据类型不包括逗号,那么,我们还可以将遍历容器所用的变量定义在foreach内部。如下所示:

[cpp]  view plain  copy
  1. QLinkedList list;  
  2. ...  
  3. foreach (const QString &str, list)  
  4.     qDebug() << str;  
同样,类似于c++的for循环,当有多条语句时,也可以使用花括号和break关键字:

[cpp]  view plain  copy
  1. QLinkedList list;  
  2. ...  
  3. foreach (const QString &str, list) {  
  4.     if (str.isEmpty())  
  5.         break;  
  6.     qDebug() << str;  
  7. }  
而对于QMap和QHash来说,foreach会访问其中存储的key-value对的value部分。如果你想同时获得key和value,可以使用迭代器,或者先获得其中的key,在通过key取到对应的值。如下代码所示:

[cpp]  view plain  copy
  1. QMapint> map;  
  2. ...  
  3. foreach (const QString &str, map.keys())  
  4.     qDebug() << str << ':' << map.value(str);  
而对于多值关联的map来说,可以使用两个foreach,如下方式访问:

[cpp]  view plain  copy
  1. QMultiMapint> map;  
  2. ...  
  3. foreach (const QString &str, map.uniqueKeys()) {  
  4.     foreach (int i, map.values(str))  
  5.         qDebug() << str << ':' << i;  
  6. }  
在进入一个foreach循环时,Qt会自动拿到容器的一个拷贝。所以,如果你在foreach的过程中,修改了容器,并不会影响这个循环。
而因为foreach会创建出一份容器的拷贝,所以使用一个非常量引用并不会使你能够修改原始容器。而仅仅会影响到拷贝,这可能不是你想要的结果。

所以,相对于Qt的foreach,一个可选的方案是C++11中的基于范围的for循环。但是,要注意的是,基于范围的for循环可以会强制一个Qt容器脱离隐式共享,而foreach不会。但是,使用foreach总是会拷贝容器,这个代价对STL的容器来说,通常是昂贵的。所以,一般,我们可以对Qt容器使用foreach关键字,而对于STL的容器使用基于范围的for循环。而除了上面的foreach之外,Qt还为无限循环提供了一个伪关键字:forever。使用如下:

[cpp]  view plain  copy
  1. forever {  
  2.     //一直执行的代码  
  3. }  
当然,如果你担心这些Qt特定的关键字会导致名称空间的污染,也可以禁用跌这些宏。只需在.pro文件中添加如下一句即可:

[cpp]  view plain  copy
  1. CONFIG += no_keywords  


其他的类容器类

Qt中包含三个模板类,其在某些方面类似于容器。但这些类不提供迭代器,也不能用于foreach关键字。

  • QVarLengthArray:该类提供了一个低级的变长数组。在某些非常看重访问速度的情况下,可以使用该类替代QVector。
  • QCache:该类提供了一个存储key-value对的缓存
  • QContiguousCache:该类提供了一种高效的缓存数据的方式,其是使用连续的内存进行访问。
  • QPair:用来存储元素对。


算法复杂度

算法复杂度是当容器中的元素增多时每一个函数的运行速度是多块或多慢。例如,在QLinkedList的中间插入一个元素是一个非常快速的操作,而不管当前链表中有多少元素。另一方面,在QVector的中间插入一个元素效率就是非常低下的,特别是当QVector中已经有了大量的元素,因为,这个操作会导致QVector中一半的元素都要在内存中移动一个位置。

为了描述算法复杂度,我们使用以下几个术语,基于"big O":

  • 常量时间:O(1)。若无论容器中有多少元素,一个函数总能在相同的时间内执行完,那么这个函数就是常量时间复杂度的。例如,QLinkedList::insert()。
  • 对数时间:O(log n)。一个函数以对数时间运行,是说它的运行时间是和容器中的元素的个数成对数相关的。例如,二分查找qBinaryFind()。
  • 线性时间:O(n)。一个函数以线性时间运行,是说它的运行时间和容器中存储的元素个数成正相关。例如QVector::insert()。
  • 线性对数时间:O(nlog n)。一个函数以线性对数时间运行,是说它的运行会随着容器中元素个数的增多逐渐慢于线性时间函数,但快于二次方复杂度的函数。
  • 二次方时间:O(n2)。一个函数以二次方时间复杂度运行,是说它的运行时间和容器中元素的个数的二次方成正相关。
下面这个表格,总结了Qt中的序列容器的算法复杂度:
  Index lookup 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.  O(1)"意思是如果你只调用函数一次,复杂度可能是O(n) ,但是如果你多次调用函数,平均复杂度则为O(1)。

下面的表格总结了Qt中的关联容器的算法复杂度:

对于QVector,QHash,和QSet,追加一个元素的性能平均是O(n)。不过,我们可以在真正插入元素之前,使用将要存储的元素数目来调用QVector::reserve(),QHash::reserve()或者QSet::reserve()把这个复杂度降低到O(1)。

生长策略

QVector,QString,和QByteArray会将它们的内容存储在连续的内存区中。QList维护一个指向所存储元素的指针数组,以此提供快速的基于下标的访问(除非T是一个 指针类型或一个大小等于指针大小的基本类型,在这种情况下,元素本身会被存储在数组里面。);QHash保存一个hash表,其大小和元素的个数有关。同时,为了避免每次添加元素都导致内存的重新分配,这个容器类通常会分配比实际情况更多的内存。

考虑下面的代码段,其从一个QString构建了一个QString:

[cpp]  view plain  copy
  1. QString onlyLetters(const QString &in)  
  2. {  
  3.     QString out;  
  4.     for (int j = 0; j < in.size(); ++j) {  
  5.         if (in[j].isLetter())  
  6.             out += in[j];  
  7.     }  
  8.     return out;  
  9. }  
我们动态的构建了一个QString out对象,使用一次追加一个字符的方式。我们先假定要向QString追加15000个字符。在此过程中会进行18次内存的重新分配(而不是15000次),分别是:4,8,16,20,52,116,244,500,1012,2036,4084,6132,8180,10288,12276,14324,16372。到最后,QString对象有16372个Unicode字符空间被分配,其中的15000个被占用。

上面的这些内存数值可能很奇怪,但下面有一些指导原则:

  • QString会一次分配4个字符,直到其大小达到20。
  • 从20到4084,按每次扩大一倍的方式增长。更准确的说,它增长到下一个2的n次方,在减去12。20=2^5-12,52=2^6-12,等等。(减去12是因为有些内存分配器需要 使用一些字节为每个内存块做簿记。)
  • 从4048开始,每次增加2048个字符(4096个字节)。这是有意义的,因为现代的操作系统在重新分配一个buffer时并不会拷贝所有的数据;只是物理内存页的简单排序,只有位于第一页和最后一页的数据需要拷贝。
QByteArray和QList使用的分配算法和QString类似。
至于QVector,当其中存储的数据类型是可以使用memcpy()函数在内存移动时(包括C++基本类型,指针类型,Qt的共享类),也使用和QString类似的分配算法。但当其中存储的类型只有通过调用构造函数和析构函数才能在内存中移动时,会使用不同的分配算法。因为在这种情况下,内存重分配的代价很高,QVector会在内存用尽后总是增长一倍的内存,从而减少内存重分配的次数。
QHash是完全不同的情况。QHash内部的哈希表已2的次方增长,并且每次增长时,其中的元素会被重新分配到一个新的桶中,计算方式为qHash(key) % QHash::capacity()(桶的数量)。这个方式也应用于QSet和QCache。

对大部分应用程序来说,Qt提供的默认生长算法就足够了。如果你需要更多的控制,QVector,QHash,QSet,QString和QByteArray提供了三个函数,允许你去检查和指定你需要多少内存去存储元素:
  • capacity():基于已分配的内存,返回元素的个数(对QHash和QSet来说,就是哈希表中桶的数量)
  • reserve(size):显式的预分配size个元素的内存
  • squeeze():释放为存储元素的多余内存。
如果你知道大约将在容器中存储多少个元素,你可以在开始插入元素前调用reserve(),预分配好需要的空间;然后,在完全插入元素后,可以调用squeeze()来释放多余的内存。

你可能感兴趣的:(Qt,qt5)