Qt 学习之路 2(39):遍历容器

上一节我们大致了解了有关存储容器的相关内容。对于所有的容器,最常用的操作就是遍历。本章我们将详细了解有关遍历器的内容。

尽管这个问题不是本章需要考虑的,但是我们还是需要来解释下,为什么要有遍历器。没有遍历器时,如果我们需要向外界提供一个列表,我们通常会将其返回:

 
 
QList<int> intlist() const
{
return list;
}
1
2
3
4
QList<int> intlist() const
{
    return list;
}

这么做的问题是:向用户暴露了集合的内部实现。用户知道,原来你用的就是一个QList啊~那我就可以向里面增加东西了,或者修改其中的内容。有时这不是我们所期望的。很多时候,我们只是想提供用户一个集合,只允许用户知道这个集合中有什么,而不是对它进行修改。为此,我们希望有这么一种对象:通过它就能够提供一种通用的访问集合元素的方法,不管底层的集合是链表还是散列,都可以通过这种对象实现。这就是遍历器。

Qt 的容器类提供了两种风格的遍历器:Java 风格和 STL 风格。这两种风格的遍历器在通过非 const 函数对集合进行修改时都是不可用的。

Java 风格的遍历器

Java 风格的遍历器是在 Qt4 首先引入的,是 Qt 应用程序首先推荐使用的形式。这种风格比起 STL 风格的遍历器更方便。方便的代价就是不如后者高效。它们的 API 非常类似于 Java 的遍历器类,故名。

每一种容器都有两种 Java 风格的遍历器:一种提供只读访问,一种提供读写访问:

容器 只读遍历器 读写遍历器
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<T> QMutableMapIterator<T>
QHash<Key, T>,QMultiHash<Key, T> QHashIterator<T> QMutableHashIterator<T>

这里我们只讨论QListQMap的遍历器。QLinkedListQVectorQSet的遍历器接口与QList的是一样的;QHash遍历器的接口则同QMap是一样的。

不同于下面我们将要介绍的 STL 风格的遍历器,Java 风格的遍历器指向的是两个元素之间的位置,而不是指向元素本身。因此,它们可能会指向集合第一个元素之前的位置,也可能指向集合的最后一个元素之后的位置,如下图所示:

我们通过下面的代码看看如何使用这种遍历器:

 
 
QList<QString> list;
list << "A" << "B" << "C" << "D";

QListIterator<QString> i(list);
while (i.hasNext()) {
qDebug() << i.next();
}

1
2
3
4
5
6
7
QList<QString> list;
list << "A" << "B" << "C" << "D";
 
QListIterator<QString> i(list);
while (i.hasNext()) {
    qDebug() << i.next();
}

首先,我们使用 list 对象创建一个遍历器。刚刚创建完成时,该遍历器位于第一个元素之前(也就是 A 之前)。我们通过调用hasNext()函数判断遍历器之后的位置上有无元素。如果有,调用next()函数将遍历器跳过其后的元素。next()函数返回刚刚跳过的元素。当然,我们也可以使用hasPrevious()previous()函数来从尾部开始遍历,详细内容可以参考 API 文档。

QListIterator是只读遍历器,不能插入或者删除数据。如果需要这些操作,我们可以使用QMutableListIterator。来看下面的代码:

 
 
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() % 2 != 0) {
i.remove();
}
}
1
2
3
4
5
6
QMutableListIterator<int> i(list);
while (i.hasNext()) {
    if (i.next() % 2 != 0) {
        i.remove();
    }
}

这段代码使用QMutableListIterator遍历集合,如果其值是奇数则将其删除。在每次循环中都要调用next()函数。正如前面所说,它会跳过其后的一个元素。remove()函数会删除我们刚刚跳过的元素。调用remove()函数并不会将遍历器置位不可用,因此我们可以连续调用这个函数。向前遍历也是类似的,这里不再赘述。

如果我们需要修改已经存在的元素,使用setValue()函数。例如:

 
 
QMutableListIterator<int> i(list);
while (i.hasNext()) {
if (i.next() > 128) {
i.setValue(128);
}
}
1
2
3
4
5
6
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;
}
1
2
3
4
QMutableListIterator<int> i(list);
while (i.hasNext()) {
    i.next() *= 2;
}

QMapItrator也是类似的。例如,使用QMapItrator我们可以将数据从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());
}

1
2
3
4
5
6
7
8
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 风格的遍历器

STL 风格的遍历器从 Qt 2.0 就开始提供。这种遍历器能够兼容 Qt 和 STL 的通用算法,并且为速度进行了优化。同 Java 风格遍历器类似,Qt 也提供了两种 STL 风格的遍历器:一种是只读访问,一种是读写访问。我们推荐尽可能使用只读访问,因为它们要比读写访问的遍历器快一些。

容器 只读遍历器 读写遍历器
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 风格的遍历器具有类似数组指针的行为。例如,我们可以使用 ++ 运算符让遍历器移动到下一个元素,使用 * 运算符获取遍历器所指的元素。对于QVectorQStack,虽然它们是在连续内存区存储元素,遍历器类型是typedef T *const_iterator类型则是typedef const T *

我们还是以QListQMap为例,理由如上。下面是有关QList的相关代码:

 
 
QList<QString> list;
list << "A" << "B" << "C" << "D";

QList<QString>::iterator i;
for (i = list.begin(); i != list.end(); ++i) {
*i = (*i).toLower();
}

1
2
3
4
5
6
7
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 风格遍历器的示意图:

我们使用const_iterator进行只读访问,例如:

 
 
QList<QString>::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i) {
qDebug() << *i;
}
1
2
3
4
QList<QString>::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i) {
    qDebug() << *i;
}

QMapQHash的遍历器,* 运算符返回集合键值对。下面的代码,我们打印出QMap的所有元素:

 
 
QMap<int, int> map;

QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
qDebug() << i.key() << ":" << i.value();
}

1
2
3
4
5
6
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 包含了很多以值的形式返回QListQStringList的函数(例如QSplitter::sizes())。如果你希望使用 STL 风格的遍历器遍历这样的元素,应该使用遍历器遍历容器的拷贝,例如:

 
 
// 正确的方式
const QList<QString> sizes = splitter->sizes();
QList<QString>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
...

// 错误的方式
QList<QString>::const_iterator i;
for (i = splitter->sizes().begin();
i != splitter->sizes().end(); ++i)
...

1
2
3
4
5
6
7
8
9
10
11
// 正确的方式
const QList<QString> sizes = splitter->sizes();
QList<QString>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
    ...
 
// 错误的方式
QList<QString>::const_iterator i;
for (i = splitter->sizes().begin();
     i != splitter->sizes().end(); ++i)
    ...

对于那些返回集合的 const 或非 const 引用的函数,就不存在这个问题。

另外,隐式数据共享对 STL 风格遍历器造成的另一个影响是,当一个容器正在被一个遍历器遍历的时候,不能对这个容器进行拷贝。如果你必须对其进行拷贝,那么就得万分小心。例如,

 
 
 
 
 
 

C++

 
QVector<int> a, b;
a.resize(100000); // 使用 0 填充一个非常大的 vector

QVector<int>::iterator i = a.begin();
// 使用遍历器 i 的错误方式(注意,此时,a 上面已经有一个正在遍历的遍历器):
b = a;
/*
现在,我们的万分小心遍历器 i。因为它指向了共享的数据。
如果我们执行语句 *i = 4,我们就会改变了共享的数据实例(两个 vector 都会被改变)。
这里的行为与 STL 容器不同,因此这种问题仅出现在 Qt 中;使用 STL 标准容器不存在这个问题。
*/

a[0] = 5;
/*
现在,容器 a 被修改了,其实际数据已经与共享数据不同,
即使 i 就是从容器 a 创建的遍历器,但是它指向的数据与 a 并不一致,其表现就像是 b 的遍历器。
这里的情形是:(*i) == 0.
*/

b.clear(); // 现在我们清空 b,此时,遍历器 i 已经不可用了。

int j = *i; // 无定义行为!
/*
来自 b 的数据(也就是 i 指向的那些数据)已经被销毁了。
这种行为在 STL 容器中是完全可行的(在 STL 容器中,(*i) == 5),
但是使用 QVector 则很有可能出现崩溃。
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
QVector<int> a, b;
a.resize(100000); // 使用 0 填充一个非常大的 vector
 
QVector<int>::iterator i = a.begin();
// 使用遍历器 i 的错误方式(注意,此时,a 上面已经有一个正在遍历的遍历器):
b = a;
/*
    现在,我们的万分小心遍历器 i。因为它指向了共享的数据。
    如果我们执行语句 *i = 4,我们就会改变了共享的数据实例(两个 vector 都会被改变)。
    这里的行为与 STL 容器不同,因此这种问题仅出现在 Qt 中;使用 STL 标准容器不存在这个问题。
*/
 
a[0] = 5;
/*
    现在,容器 a 被修改了,其实际数据已经与共享数据不同,
    即使 i 就是从容器 a 创建的遍历器,但是它指向的数据与 a 并不一致,其表现就像是 b 的遍历器。
    这里的情形是:(*i) == 0.
*/
 
b.clear(); // 现在我们清空 b,此时,遍历器 i 已经不可用了。
 
int j = *i; // 无定义行为!
/*
    来自 b 的数据(也就是 i 指向的那些数据)已经被销毁了。
    这种行为在 STL 容器中是完全可行的(在 STL 容器中,(*i) == 5),
    但是使用 QVector 则很有可能出现崩溃。
*/

虽然这个例子只演示了QVector,但实际上,这个问题适用于所有隐式数据共享的容器类。

foreach关键字

如果我们仅仅想要遍历集合所有元素,我们可以使用 Qt 的foreach关键字。这个关键字是 Qt 特有的,通过预处理器进行处理。C++ 11 也提供了自己的foreach关键字,不过与此还是有区别的。

foreach的语法是foreach (variable, container)。例如,我们使用foreachQLinkedList进行遍历:

 
 
QLinkedList<QString> list;
...
QString str;
foreach (str, list) {
qDebug() << str;
}
1
2
3
4
5
6
QLinkedList<QString> list;
...
QString str;
foreach (str, list) {
    qDebug() << str;
}

这段代码与下面是等价的:

 
 
QLinkedList<QString> list;
...
QLinkedListIterator<QString> i(list);
while (i.hasNext()) {
qDebug() << i.next();
}
1
2
3
4
5
6
QLinkedList<QString> list;
...
QLinkedListIterator<QString> i(list);
while (i.hasNext()) {
    qDebug() << i.next();
}

如果类型名中带有逗号,比如QPair<int, int>,我们只能像上面一样,先创建一个对象,然后使用foreach关键字。如果没有逗号,则可以直接在foreach关键字中使用新的对象,例如:

 
 
QLinkedList<QString> list;
...
foreach (const QString &str, list) {
qDebug() << str;
}
1
2
3
4
5
QLinkedList<QString> list;
...
foreach (const QString &str, list) {
    qDebug() << str;
}

Qt 会在foreach循环时自动拷贝容器。这意味着,如果在遍历时修改集合,对于正在进行的遍历是没有影响的。即使不修改容器,拷贝也是会发生的。但是由于存在隐式数据共享,这种拷贝还是非常迅速的。

因为foreach创建了集合的拷贝,使用集合的非 const 引用也不能实际修改原始集合,所修改的只是这个拷贝。

Qt 学习之路 2(39):遍历容器的更多相关文章

  1. Qt 学习之路 2(38):存储容器

    Qt 学习之路 2(38):存储容器 豆子 2013年1月14日 Qt 学习之路 2 38条评论 存储容器(containers)有时候也被称为集合(collections),是能够在内存中存储其它特 ...

  2. Qt 学习之路 2(5):自定义信号槽

    Home / Qt 学习之路 2 / Qt 学习之路 2(5):自定义信号槽 Qt 学习之路 2(5):自定义信号槽  豆子  2012年8月24日  Qt 学习之路 2  131条评论 上一节我们详 ...

  3. Qt 学习之路 2(40):隐式数据共享

    Qt 学习之路 2(40):隐式数据共享 豆子 2013年1月21日 Qt 学习之路 2 14条评论 Qt 中许多 C++ 类使用了隐式数据共享技术,来最大化资源利用率和最小化拷贝时的资源消耗.当作为 ...

  4. Qt 学习之路 2(33):贪吃蛇游戏(3)

    Qt 学习之路 2(33):贪吃蛇游戏(3) 豆子 2012年12月29日 Qt 学习之路 2 16条评论 继续前面一章的内容.上次我们讲完了有关蛇的静态部分,也就是绘制部分.现在,我们开始添加游戏控 ...

  5. Qt 学习之路 2(32):贪吃蛇游戏(2)

    Qt 学习之路 2(32):贪吃蛇游戏(2) 豆子 2012年12月27日 Qt 学习之路 2 55条评论 下面我们继续上一章的内容.在上一章中,我们已经完成了地图的设计,当然是相当简单的.在我们的游 ...

  6. 《Qt 学习之路 2》目录

    <Qt 学习之路 2>目录 <Qt 学习之路 2>目录  豆子  2012年8月23日  Qt 学习之路 2  177条评论 <Qt 学习之路 2>目录 序 Qt ...

  7. Qt 学习之路 2(72):线程和事件循环

    Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻>  --  有需求的话还需要进行专题学习  豆子  2013年11月24日  Qt 学习之路 2  34条评论 前面一章我 ...

  8. Qt 学习之路 2(71):线程简介

    Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...

  9. Qt 学习之路 2(67):访问网络(3)

    Qt 学习之路 2(67):访问网络(3) 豆子 2013年11月5日 Qt 学习之路 2 16条评论 上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作.本章我们将继续完 ...

随机推荐

  1. WordPress,discuz 根据不同的入口url更换logo

    Discuz!中调用cookie的思路出来了:    设置cookie:dsetcookie('cookie名', 'cookie值', '有效时间'); 读取cookie有两种方法,第一种使用get ...

  2. laravel 中的Gates,以及修改模型

    Gates 是一个用于判断用户是否有权进行某项操作的闭包,通常使用Gate 门面定义在 App\Providers\AuthServiceProvider类中.Gates 总是接收用户实例作为第一个参 ...

  3. 面试题:Java程序员最常用的20%技术 已看1

    首先常用api(String,StringBuffer/StringBuilder等) 1.集合类,线程类 2.Servlet(很少用纯粹的servlet写,但你要懂,因为很多框架都是基于servle ...

  4. 面试题:JavaIO流分类详解与常用流用法实例

    Java流概念: Java把所有的有序数据都抽象成流模型,简化了输入输出,理解了流模型就理解了Java IO.可以把流想象成水流,里面的水滴有序的朝某一方向流动.水滴就是数据,且代表着最小的数据流动单 ...

  5. open source libraries: BIAS

    The Basic Image AlgorithmS C/C++ Library (BIAS) is the code base for research and software developme ...

  6. Docker学习之路(三)Docker网络详解

    1. Docker的4种网络模式 我们在使用docker run创建Docker容器时,可以用--net选项指定容器的网络模式,Docker有以下4种网络模式: host模式,使用--net=host ...

  7. Luogu 3723 [AH2017/HNOI2017]礼物

    BZOJ 4827 $$\sum_{i = 1}^{n}(x_i - y_i + c)^2 = \sum_{i = 1}^{n}(x_i^2 + y_i^2 + c^2 - 2 * x_iy_i + ...

  8. 半平面交 (poj 1279(第一道半平面NlogN)完整注释 )

    半平面交的O(nlogn)算法(转载) 求n个半平面的交有三种做法: 第一种就是用每个平面去切割已有的凸多边形,复杂度O(n^2). 第二种就是传说中的分治算法.将n个半平面分成两个部分,分别求完交之 ...

  9. Java主线程如何等待子线程执行结束(转)

    工作中往往会遇到异步去执行某段逻辑, 然后先处理其他事情, 处理完后再把那段逻辑的处理结果进行汇总的产景, 这时候就需要使用线程了. 一个线程启动之后, 是异步的去执行需要执行的内容的, 不会影响主线 ...

  10. 如何在sqlserver 的函数或存储过程中抛出异常。

    raiserror 的作用: raiserror 是用于抛出一个错误.[ 以下资料来源于sql server 2005的帮助 ] 其语法如下: RAISERROR ( { msg_id | msg_s ...