Container Classes
Qt库提供了一组基于模板的一般化的容器类。这些容器可以存储指定的类型的元素。例如,如果你需要一个可变大小的Qstring数组,可以用QVector<QString>.。
这些容器比STL容器更轻更安全更容易使用。如果你不熟悉STL或者更喜欢以Qt的方式做事,你可以用这些类取代STL类。
这些类是隐式共享的,它们都是可重入,它们进行了速度优化,用更少的内存和最小的内联代码扩展,生成更小的可执行文件。此外,当所有的线程仅仅以只读的方式访问它们时,它们是线程安全的。
为了遍历容器的元素,你可以用 Java-style iterators 或者 STL-style iterators. 。 Java-style iterators更容易使用和提供更高级的函数。STL-style iterators 效率稍微高一些,而且可以喝Qt的或STL的通用算法一起使用。
Qt也提供关键字foreach 使得很容易遍历容易内的元素。
Qt提供了以下顺序容器:QList, QLinkedList, QVector, QStack,和 QQueue. 。对于大多数程序而言,QList 是最好用的,即使它以数组列表实现,它提供了非常快的在前面和后面追加的函数。如果你真的需要一个连接表,可以用QLinkedList;。如果你想让元素占用连续的内存空间,可以用 QVector. QStack and QQueue,它们提供了后进先出和先进先出。
Qt也提供了关联式容器: QMap, QMultiMap, QHash, QMultiHash,and QSet. 。Multi容器支持多个value关联到单一的key。Hash容器提供了通过hash函数快速查找取代二分查找。
特殊情况下, QCache 和 QContiguousCache 提供了在有限的cache 中高效的查找对象。
Class |
Summary |
QList<T> |
This is by far the most commonly used container class. It stores a list of values of a given type (T) that can be accessed by index. Internally, the QList is implemented using an array, ensuring that index-based access is very fast. Items can be added at either end of the list using QList::append() and QList::prepend(), or they can be inserted in the middle using QList::insert(). More than any other container class,QList is highly optimized to expand to as little code as possible in the executable.QStringList inherits from QList<QString>. |
QLinkedList<T> |
This is similar to QList, except that it uses iterators rather than integer indexes to access items. It also provides better performance than QList when inserting in the middle of a huge list, and it has nicer iterator semantics. (Iterators pointing to an item in a QLinkedList remain valid as long as the item exists, whereas iterators to a QList can become invalid after any insertion or removal.) |
QVector<T> |
This stores an array of values of a given type at adjacent positions in memory. Inserting at the front or in the middle of a vector can be quite slow, because it can lead to large numbers of items having to be moved by one position in memory. |
QStack<T> |
This is a convenience subclass of QVector that provides "last in, first out" (LIFO) semantics. It adds the following functions to those already present in QVector: push(), pop(), and top(). |
QQueue<T> |
This is a convenience subclass of QList that provides "first in, first out" (FIFO) semantics. It adds the following functions to those already present in QList: enqueue(), dequeue(), and head(). |
QSet<T> |
This provides a single-valued mathematical set with fast lookups. |
QMap<Key, T> |
This provides a dictionary (associative array) that maps keys of type Key to values of type T. Normally each key is associated with a single value. QMap stores its data in Key order; if order doesn't matter QHash is a faster alternative. |
QMultiMap<Key, T> |
This is a convenience subclass of QMap that provides a nice interface for multi-valued maps, i.e. maps where one key can be associated with multiple values. |
QHash<Key, T> |
This has almost the same API as QMap, but provides significantly faster lookups. QHash stores its data in an arbitrary order. |
QMultiHash<Key, T> |
This is a convenience subclass of QHash that provides a nice interface for multi-valued hashes. |
容器可以嵌套。例如,当key的类型是Qstring和value类型是Qlist<int>,完全可以用 QMap<QString, QList<int>>。缺陷就是必须在> >之间插入空格。否则C++编译器会错把> >,翻译成右移位操作并报告语法错误。
这些容器被定义在不同的头文件。为了方便,这些容器在文件<QtContainerFwd>.进行了前置声明。
容器保存的类型可以是任意可赋值的数据类型。因此要用容器存储,一个类型必须提供默认构造函数,拷贝构造函数和赋值操作。这包含了大部分可能用容器存储的数据类型,包括基本类型如int和double,指针和Qt数据类型如Qstring,Qdate和Qtime。但是不包含QObject以及任意QObject的子类。如果你视图实例化QList<QWidget>,编译器将抱怨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;
QStringtitle;
QDatereleaseDate;
};
一些容器对于要进行存储的数据类型有额外的要求,例如,QMap<Key,T>的key类型必须提供operator<()。这样的特殊要求在类的详细描述中都有介绍。在一些情况下,特殊的函数有特殊的要求,这些只是每个函数的基础描述,当要求不满足时编译器将报错。
Qt容器提供operator<<() 和operator>>() 所以可以很容易用 QDataStream进行读写。这意味着存储在容器中的数据类型也必须支持operator<<() 和 operator>>()。这样的支持很简单:
QDataStream&operator<<(QDataStream&out, const Movie &movie)
{
out << (quint32)movie.id<< movie.title
<< movie.releaseDate;
return out;
}
QDataStream&operator>>(QDataStream&in, Movie &movie)
{
quint32id;
QDatedate;
in >> id >> movie.title>> date;
movie.id = (int)id;
movie.releaseDate = date;
return in;
}
某些容器类的函数的文档说明设计默认构造值,例如, QVector用默认构造值自动的初始化其元素。如果没有指定key值,则QMap::value() 返回默认构造值。对于大多数值类型,这意味着简单的用默认构造函数创建默认值。但是对于原始类型如int,double和指针,C++语言不指定任何初始化,在这种情况下,Qt容器自动的初始化为0.
迭代器提供了访问容器元素的统一方法。Qt容器提供两种迭代器:Java-style iterators and STL-style iterators.。当容器中的数据被修改或者由于调用非const成员函数生成隐式共享数据副本时,这两种迭代器都会无效。
Java-style iterators是在Qt4才新加入的而且Qt程序中标准的使用着。它们比STL-styleiterators用起来方便,但是效率稍微低了一点。它们的API仿照java的迭代器类。
对于美国容器类,有两种Java-style iterator数据类型,一种是只读,一种可读写:
ContainersRead-only iteratorRead-write
Containers |
Read-only iterator |
Read-write iterator |
QList<T>, QQueue<T> |
QListIterator<T> |
QMutableListIterator<T> |
QLinkedList<T> |
QLinkedListIterator<T> |
QMutableLinkedListIterator<T> |
QVector<T>, QStack<T> |
QVectorIterator<T> |
QMutableVectorIterator<T> |
QSet<T> |
QSetIterator<T> |
QMutableSetIterator<T> |
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-style iterators 不同的是, Java-style iterators指向元素的中间而不是直接指向元素的。因此,它们指向容器的开头,容器的末尾或者两个元素的之间。如下图所示:
以下是典型的遍历 QList<QString>并输出到控制台的循环:
QList<QString>list;
list << "A" << "B" << "C" <<"D";
QListIterator<QString>i(list);
while (i.hasNext())
qDebug()<< i.next();
它的工作原理如下:QList 的迭代传递到 QListIterator 的构造函数。此时,迭代器指向list的第一个元素之前。然后调用 hasNext() 检测迭代器后面是否有元素,如果有,我们调用next() 跳过该元素,next() 返回所跳过的元素。对于 QList<QString>,其元素的类型是 QString.。
下面是反向遍历QList:
QListIterator<QString>i(list);
i.toBack();
while (i.hasPrevious())
qDebug()<< i.previous();
这段代码与正向遍历的对称,除了开始调用toBack() 把迭代器移到最后元素的后面。
下图说明了调用 next() 和 previous() 的效果:
下面总结了 QListIterator API:
Function |
Behavior |
toFront() |
Moves the iterator to the front of the list (before the first item) |
toBack() |
Moves the iterator to the back of the list (after the last item) |
hasNext() |
Returns true if the iterator isn't at the back of the list |
next() |
Returns the next item and advances the iterator by one position |
peekNext() |
Returns the next item without moving the iterator |
hasPrevious() |
Returns true if the iterator isn't at the front of the list |
previous() |
Returns the previous item and moves the iterator back by one position |
peekPrevious() |
Returns the previous item without moving the iterator |
QListIterator不提供插入或删除元素的函数。要实现插入删除,可以用QMutableListIterator,下面例子从 QList<int> 中删除奇书:
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() % 2 != 0)
i.remove();
}
循环中每次调用next()都跳过后面的元素。remove() 函数从list中删除最近跳过的元素。调用remove() 不会是迭代器失效,所以可以安全的迹象使用它。反向遍历也一样:
QMutableListIterator<int> i(list);
i.toBack();
while (i.hasPrevious()) {
if (i.previous() % 2 != 0)
i.remove();
}
如果想改变存在的一个元素,可以用setValue().。
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的迭代器的API 跟 QList的一样。现在来讨论 QMapIterator,它遍历(key,value)对,多少有些不同。
像 QListIterator一样,, QMapIterator 提供 toFront(), toBack(), hasNext(), next(), peekNext(), hasPrevious(), previous(),和 peekPrevious(). 。key和value通过next(),peekNext(), previous(), 或者 peekPrevious().的返回对象调用key()和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();
}
QMapIterator 也提供对于迭代器的key()和value()函数,返回最近被跳过元素的key和value。
QMap<int, QWidget*> map;
QHash<int, QWidget*> hash;
QMapIterator<int, QWidget*> i(map);
while (i.hasNext()) {
i.next();
hash.insert(i.key(), i.value());
}
如果想遍历有相同value的元素,可以用 findNext() 或者 findPrevious().。
QMutableMapIterator<int, QWidget*> i(map);
while (i.findNext(widget))
i.remove();
STL-style iterators 从Qt2.0开发使用。它们与Qt的和STL的通用算法兼容,并进行了速度优化。
对于每个容器,都有两种STL-style iterator类型:一种是只读,一种是可读写的。应该进可能的使用只读的迭代器,因为它比可读写的迭代器快。
Containers |
Read-only iterator |
Read-write iterator |
QList<T>, QQueue<T> |
QList<T>::const_iterator |
QList<T>::iterator |
QLinkedList<T> |
QLinkedList<T>::const_iterator |
QLinkedList<T>::iterator |
QVector<T>, QStack<T> |
QVector<T>::const_iterator |
QVector<T>::iterator |
QSet<T> |
QSet<T>::const_iterator |
QSet<T>::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 iterators 的API参照数组的指针。例如,++操作将迭代器指向下一个元素,* 操作返货迭代器指向的元素。实际上,QVector 和 QStack,的元素存储在相邻的内存位置, iterator 仅仅是T *,的别名, const_iterator 是 constT *.的别名。
在此讨论中,我们只关注 QList 和 QMap., QLinkedList, QVector,和 QSet 的迭代器与 QList的完全一样,同样的, QHash 的迭代器和 QMap'的完全一样。
QList<QString>list;
list << "A" << "B" << "C" <<"D";
QList<QString>::iteratori;
for (i = list.begin(); i != list.end(); ++i)
*i = (*i).toLower();
与 Java-styleiterators不同的是, STL-style iterators 直接指向元素,begin()返回指向容器的第一个元素的迭代器。end() 返回一个迭代器指向超过容器最后一个元素的虚拟的元素。end() 标记无效的位置,不能解引用,典型的用于循环终止条件。如果list是空的,begin() 和 end(),相等,循环不会被执行。
下图的红色箭头显示的是有效的迭代器位置:
用STL-style iterator 进行反向迭代在访问元素之前必须先自减。
QList<QString>list;
list << "A" << "B" << "C" <<"D";
QList<QString>::iteratori = list.end();
while (i != list.begin()) {
--i;
*i = (*i).toLower();
}
到目前为止的代码中,我们用* 操作获取元素的值,然后调用了QString::toLower() 。大部分的编译器也支持 i->toLower(),,有的则不支持。
对于只读,我们用const_iterator, constBegin(), 和 constEnd()
QList<QString>::const_iteratori;
for (i = list.constBegin(); i != list.constEnd(); ++i)
qDebug()<< *i;
下表总结了STL-style iterators' API:
Expression |
Behavior |
*i |
Returns the current item |
++i |
Advances the iterator to the next item |
i += n |
Advances the iterator by n items |
--i |
Moves the iterator back by one item |
i -= n |
Moves the iterator back by n items |
i - j |
Returns the number of items between iterators i and j |
++和—操作都可以用做前缀和后缀。前缀修改迭代器并返回修改后的迭代器,后缀版本先复制迭代器,再修改,返回复制的副本。当忽略返回值的表达式中,我们建议使用前缀版本,因为它比较快一些。
对于非const迭代器类型,*操作的返回值可以用作复制操作的左值。
对于QMap 和 QHash, *操作返回元素的value部分。如果你想获得key,可以对迭代器调用key()。对应的,迭代器也提供value()函数来获得value。
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 迭代器遍历它们,应该先拷贝容器并遍历拷贝的容器副本:
// 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引用的函数并不会出现这种情况。
隐式共享在STL-style iterators的另外一个结果是:当容器正在使用非const迭代器时不能拷贝容器, Java-style iterators不受此限制。
如果你只是想依次的遍历容器的元素,可以用Qt的关键字foreach ,该关键字是Qt对C++额外添加的,用预处理器实现的。它的语法为:foreach (variable, container)
QLinkedList<QString>list;
...
QStringstr;
foreach(str, list)
qDebug()<< str;
foreach 代码比使用迭代器的同等代码要短:
QLinkedList<QString>list;
...
QLinkedListIterator<QString>i(list);
while (i.hasNext())
qDebug()<< i.next();
如果数据类型不包含逗号,用于遍历的变量可以声明在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 获取的是 (key,value) 对的alue部分。如果你想遍历key和alue,可以用迭代器或者下如下代码:
QMap<QString,int> map;
...
foreach(const QString&str, map.keys())
qDebug()<< str << ":" << map.value(str);
For a multi-valued map:
QMultiMap<QString,int> map;
...
foreach(const QString&str, map.uniqueKeys()) {
foreach (int i, map.values(str))
qDebug()<< str << ":" << i;
}
当进入foreach 循环时Qt自动拷贝容器。如果你在遍历的时候改变了容器,也不会影响循环。(即使你不改变容器,也一样会进行拷贝容器,多亏了隐式共享拷贝容器速度很快。)
由于foreach 创建了容器的副本,所以用非const变量并不能改变原始的容器,仅仅影响容器副本,这可能不是你想要的。
另外,Qt也提供了伪关键字forever 无限循环:
forever {
...
}
如果你担心命名空间受污染,可以在 .pro文件中添加下面的代码行禁止这些宏:
CONFIG += no_keywords
Qt包含了三个在某些方面类似容器的模板类,这些类不提供迭代器,而且不能用foreach 关键字:
· QVarLengthArray<T, Prealloc> provides a low-level variable-length array. It can beused instead of QVector in places where speed is particularly important.
· QCache<Key,T> provides a cache to store objects of a certain type T associated withkeys of type Key.
· QContiguousCache<T>provides an efficient way of caching data that is typically accessed in acontiguous way.
· QPair<T1,T2> stores a pair of elements.
另外的非模板类型为: QBitArray, QByteArray, QString,和QStringList.。
算法复杂度关心当容器的元素增加时每个函数的运行有多快。例如,在QLinkedList中间插入一个元素速度是很快的,不管里面存储了多少个元素。但是,如果在QVector 中插入一个元素,如果QVector 包含有很多元素时代价可能会很大,因为一般的元素都需要移动一个位置。
为了描述算法复杂度,我们用以下基于big Oh的术语:
· Constant time: O(1). A functionis said to run in constant time if it requires the same amount of time nomatter how many items are present in the container. One example is QLinkedList::insert().
· Logarithmic time: O(log n).A function that runs in logarithmic time is a function whose running time isproportional to the logarithm of the number of items in the container. Oneexample is qBinaryFind().
· Linear time: O(n). A functionthat runs in linear time will execute in a time directly proportional to thenumber of items stored in the container. One example is QVector::insert().
· Linear-logarithmic time: O(n log n).A function that runs in linear-logarithmic time is asymptotically slower than alinear-time function, but faster than a quadratic-time function.
· Quadratic time: O(n²). Aquadratic-time function executes in a time that is proportional to the squareof the number of items stored in the container.
下表总结了Qt顺序容器类的算法复杂度:
Index lookup |
Insertion |
Prepending |
Appending |
|
QLinkedList<T> |
O(n) |
O(1) |
O(1) |
O(1) |
QList<T> |
O(1) |
O(n) |
Amort. O(1) |
Amort. O(1) |
QVector<T> |
O(1) |
O(n) |
O(n) |
Amort. O(1) |
在表中,"Amort." 代表"amortized behavior"。例如,"Amort.O(1)"意思是如果你只调用函数一次,复杂度可能是O(n) ,但是如果你多次调用函数,平均复杂度则为O(1)。
下表总结了Qt关联容器的算法复杂度:
Key lookup |
Insertion |
|||
Average |
Worst case |
Average |
Worst case |
|
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<Key> |
Amort. O(1) |
O(n) |
Amort. O(1) |
O(n) |
QVector<T>, QString,和 QByteArray 的元素存储在连续的内存中;QList<T>维护着一个指向其元素的指针数组以便快速索引元素(除非T是指针类型或者一个基本类型的指针的大小,此时值本身存在数组)。QHash<Key,T> 拥有一个hash表,hash表的大小与其中的元素的个数成比例,要避免每次在容器后面添加元素时都进行内存重新分配,这里类分配了多于实际需要的内存。
考虑下面的代码,我们从一个QString生成另一个QString:
QStringonlyLetters(const QString&in)
{
QStringout;
for (int j = 0; j < in.size(); ++j) {
if (in[j].isLetter())
out += in[j];
}
return out;
}
我们通过每次动态的添加一个字符来创建一个字符串out。假设我们添加15000个字符到QString字符串。当QString用完内存之后进行了以下18此内存再分配: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到4080其大小每次成倍增加,.更准确的说,它增加到下一个2的幂次方减去 12.
· 从4080开始,它每次增加2048个字符。这很容易理解因为现代的操作系统当再分配内存是不拷贝整个数据,物理内存页仅仅是被重新整理了,而且只有第一个和最后一个也需要拷贝。
QByteArray 和 QList<T> 用的算法有点像Qstring。
QVector<T> 对于可以用memcpy() 拷贝数据的也用那些算法,但是对于通过调用拷贝构造函数和析构函数拷贝的数据类型则用不同的算法。由于在那种情况下再分配内存的代价很高,当用完内存的时候QVector<T>通过成倍增长以减少分配次数。
QHash<Key, T>则是完全不一样的情况。Qhash的内部hash表以平方次增长的,而且每次增长的元素被重定位到新的桶中,计算为 qHash(key) % QHash::capacity()。这个备注也使用与 QSet<T> 和 QCache<Key, T> 。
对于大部分程序而言,Qt默认提供的算法是起作用的。如果你需要更多的控制,QVector<T>, QHash<Key, T>, QSet<T>, QString, and QByteArray 提供了三个函数允许你检查和指定需要用多少内存来存储元素:
· capacity() 返回分配的可以存储元素的数量的内存。
· reserve(size) 显示的预先分配size个元素的内存
· squeeze() 释放不需要存储元素的内存。
· 如果你知道容器大概要存储多少个元素,你可以调用reserve(),,而且当你完成填充容器,你可以调用squeeze() 释放多余的预先分配的内存。
·