9.5 数据容器的迭代器

本节列举数据容器对应的迭代器,Java 风格迭代器是单独的类,STL 风格的迭代器是数据容器类自己内嵌的,容器类的迭代器用法很相似。 QList、QQueue、QVector、QStack 等容器经常根据序号使用中括号运算符遍历访问元素,而其他的容器则更多地使用迭代器进行遍历。

9.5.1 STL 风格迭代器

本章所有容器类型都有 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
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
QSet<T>  QSet<T>::const_iterator QSet<T>::iterator

每个容器的 STL 风格迭代器函数都是该类的成员函数,STL 迭代器使用方法就像指针,*it 是获取元素值,而 it++ 或 ++it 就像是指向下一个元素的指针。STL 风格迭代器使用代码举例:
QList<QString> list;
list << "A" << "B" << "C" << "D";

QList<QString>::iterator i;
for (i = list.begin(); i != list.end(); ++i)
    *i = (*i).toLower();
上述代码定义了列表 list,里面存储四个大写字母;
然后定义迭代器 i,使用 for 循环进行迭代,i 初始为 list.begin() ,每轮循环将元素字符串转为小写,再赋值给元素本身,完成大写转小写;
注意循环结束的判断是  i != list.end() ,因为 end() 指向不存在的假想末尾,是不能访问的假货,不能对 end() 使用 * 运算符访问元素!
STL 迭代器遍历过程如下图所示:
STL
每轮循环,迭代器都指向元素本身,除了 end() 是假想元素不能访问,其他的都是使用 *it 访问元素本身,如果是 QMap 、QHash及其派生类,那么使用 it.key() 和 it.value() 访问键-值对的内容。
对于存储键-值对的容器,STL 风格迭代器的示例代码如下:
    QMap<int, int> map;
    map.insert(1,1);
    map.insert(2,4);
    map.insert(3,9);
    //迭代访问
    QMap<int, int>::const_iterator it;
    for(it=map.constBegin(); it!=map.constEnd(); ++it)
    {
        qDebug()<< it.key() << it.value();
        qDebug()<< *it;
    }
it.key() 返回元素的关键字内容,it.value() 返回映射值内容,映射和哈希容器也有 *it 的用法。 *it 等同于 it.value() 映射值。上述代码输出为:
1 1
1
2 4
4
3 9
9
STL 风格迭代器常见运算符使用如下表所示:
表达式 行为
*i 访问元素的数值(映射类元素的 value)
++i 移动到下一个元素,注意该运算符不检查越界,越界就是野指针
i += n 移动到后面第 n 个元素,注意该运算符不检查越界,越界就是野指针
--i 移动到前一个元素,注意该运算符不检查越界,越界就是野指针
i -= n 移动到前面第 n 个元素,注意该运算符不检查越界,越界就是野指针
i - j 对于顺序容器,计算两个迭代器 i 和 j 位置中间包括元素的个数;
如果 i 是迭代器,j 是 int,返回迭代器 i 往前第 j 个元素迭代器。
对于关联容器,j 不能是迭代器,j 只能是整数int,返回迭代器 i 往前第 j 个元素迭代器。
i.key() 和 i.value() 映射类元素的关键字 key 和映射值 value

--i 、++i 与 i--、i++ 都类似指针的用法,前一对在表达式计算之前增减数值,后一对在表达式计算之后增减数值,通常推荐使用 --i 、++i ,这对操作执行效率稍高一些。
除了从前到后迭代,也可以反过来,从后往前迭代:
QList<QString> list;
list << "A" << "B" << "C" << "D";

QList<QString>::iterator i = list.end();
while ( i != list.begin() )
{
    --i;
    *i = (*i).toLower();
}
注意迭代器移动时,要自己写代码检查是否越界,运算符功能本身不检查越界, 如果移动越界了,就是野指针访问,可能导致程序异常崩溃。

使用迭代器时,需要特别注意 Qt 类对象的隐式共享特性,如果使用不当,会产生野指针访问:
    QVector<int> a, b;
    a.resize(100); //向量初始化为100个0
    // i 指向向量第一个元素
    QVector<int>::iterator i = a.begin();
    //隐式共享,b  a 指向同一块存储空间
    b = a;
    //隐式共享在一个对象发生变化后,为变化的对象分配新空间,并赋值
    // a 元素修改后,i 迭代器与 a 无关了!!!
    // i 其实指向 b 首元素,因为 b 没有修改,使用旧的内存空间
    a[0] = 5;
    //这时候 *i  b 开头的数值 0
    b.clear(); //清空 b,那么迭代器 i 就属于野指针!!!

    // 迭代器的错误示范
    int j = *i; //野指针使用,未知结果
迭代器 i 原本指向 a 的空间,但是隐式共享特性是谁改变数值,就给谁分配新空间,修改 a[0] 之后,迭代器 i 其实已经处于异常状态,已经与 a 无关了。这时候 i 仍然指向 b,那么一旦 b 清除空间,i 就成了野指针,这是非常危险的,可能导致内存错误,程序异常结束。
因此,使用迭代器时,一定要注意使用最新的迭代器赋值,不要用旧的过期的迭代器。
隐式共享对象元素的增删改都可能导致迭代器失效,如果不清楚状况,那最好的做法是复制一个常量对象,对常量对象使用只读迭代器:
// OK
const QList<int> sizes = splitter->sizes();
QList<int>::const_iterator i;
for (i = sizes.begin(); i != sizes.end(); ++i)
    ...
如果没有使用隐式共享,只是一个对象一个存储空间,那么可以放心使用迭代器,迭代过程中删除元素应该使用专门的迭代器函数 erase(), erase() 会返回下一个元素迭代器位置,迭代循环过程中一般不要使用 remove(),remove() 可能导致之前的迭代器失效
在迭代过程正确删除元素的示范代码如下:
QHash<QString, int>::iterator i = hash.begin();
while (i != hash.end())
{
    if (i.key().startsWith("_"))
        i = hash.erase(i);
    else
        ++i;
}
或者保存旧元素的迭代器,提前移动到下一个元素位置,然后删除旧元素迭代器位置:
QHash<QString, int>::iterator i = hash.begin();
while (i != hash.end())
{
    QHash<QString, int>::iterator prev = i;
    ++i;
    if (prev.key().startsWith("_"))
        hash.erase(prev);
}

下面我们编写一个员工工资的例子,运用迭代器进行查询和遍历操作。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 salary,创建路径 D:\QtProjects\ch09,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
打开 widget.ui 文件进入图形化编辑界面,构造界面如下图所示:
ui
界面第一行是“人员工资列表”标签。第二行是一个列表控件 listWidget。
第三行是“所有人涨10%”按钮 pushButtonIncrease、“查找张三工资”按钮 pushButtonFindZhang3、“查找工资最高3人”按钮 pushButtonFindTop3;
第四行是“查找8K以上员工”按钮 pushButtonFind8K、“删除8K以上员工”按钮 pushButtonDel8K;
第三行和第四行使用网格布局器排布,2行3列,最后一个网格空的。
最后一行是一个文本浏览器 textBrowser。
窗口整体使用垂直布局器,窗口大小 420 * 480 。
窗口布局设置好之后,我们依次右击每个按钮,从右键菜单为每个按钮的 clicked() 信号添加槽函数,5 个槽函数添加之后,我们下面开始编辑代码。
我们开始编辑 头文件 widget.h 的代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QMultiHash>
#include <QMultiMap> //可以用于键值对的同步排序

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

    //初始化填充
    void InitSalary();
    //工资列表变动时,显示到列表控件
    void UpdateSalaryShow();

private slots:
    void on_pushButtonIncrease_clicked();

    void on_pushButtonFindZhang3_clicked();

    void on_pushButtonFindTop3_clicked();

    void on_pushButtonFind8K_clicked();

    void on_pushButtonDel8K_clicked();

private:
    Ui::Widget *ui;
    //使用哈希映射保存工资表
    QMultiHash<QString, double> m_salary;
};

#endif // WIDGET_H
我们添加 QMultiHash、QMultiMap 的头文件包含,QMultiMap 后面代码用于键值对的同步排序。
我们为窗口类添加一个多哈希映射类的对象 m_salary 保存工资信息。
我们为窗口类添加两个函数,InitSalary() 函数用于填充成员变量 m_salary,存储工资信息,
UpdateSalaryShow() 函数用于在成员变量 m_salary 变动时,更新列表控件显示。其他代码都是自动生成的,包括 5 个槽函数。

下面我们分块编辑 widget.cpp 源文件,首先是构造函数和两个自定义的函数部分:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //初始化工资表
    InitSalary();
    //更新显示
    UpdateSalaryShow();
}

Widget::~Widget()
{
    delete ui;
}

//初始化填充
void Widget::InitSalary()
{
    m_salary.clear();//清空
    //添加工资内容
    m_salary.insert(tr("张三"), 5000.0);
    m_salary.insert(tr("张三"), 8000.0); //有重名的
    m_salary.insert(tr("李四"), 6000.0);
    m_salary.insert(tr("王五"), 7000.0);
    m_salary.insert(tr("孙六"), 8000.0);
    m_salary.insert(tr("赵七"), 6600.0);
    m_salary.insert(tr("钱八"), 8800.0);
}

//工资列表变动时,显示到列表控件
void Widget::UpdateSalaryShow()
{
    //清空旧的内容
    ui->listWidget->clear();
    //只读迭代器遍历
    QMultiHash<QString, double>::const_iterator it;
    for( it=m_salary.constBegin(); it!=m_salary.constEnd(); ++it )
    {
        QString strLine = it.key() + tr("\t%1").arg( it.value() );
        ui->listWidget->addItem( strLine );
    }
    //完成
}
构造函数里面,调用 InitSalary() 初始化工资成员变量;调用 UpdateSalaryShow() 更新列表控件显示。
在 InitSalary() 函数内部,先清空旧的对象内容,然后简单调用 m_salary.insert() 添加人名和工资数值,人名是有重复的,因此我们使用多哈希映射类型的成员变量。
在 UpdateSalaryShow() 函数内部,先清空列表控件里旧的内容,然后使用只读迭代器,遍历 m_salary 内容,将每个元素的人名和工资构造一行文本,根据文本添加一个列表控件条目,显示到列表控件。
注意 m_salary.constEnd() 要使用不等于判断,在 it 指向末尾虚假节点时停止迭代,因为末尾虚假节点不能访问,必须终止。

接下里我们编辑“所有人涨10%”按钮对应的槽函数:
//让所有人涨工资 10%
void Widget::on_pushButtonIncrease_clicked()
{
    //读写迭代器遍历
    QMultiHash<QString, double>::iterator it;
    for( it=m_salary.begin(); it!=m_salary.end(); ++it)
    {
        //读写迭代器 it.value() 返回可以读写的 value 引用,可以作为左值写入
        it.value() *= 1.1 ;
        //注意 it.key() 永远是只读引用,不能修改key,映射的 key 只能删除或添加,不能直接修改
    }
    //更新显示
    UpdateSalaryShow();
    ui->textBrowser->setText( tr("所有人涨工资 10% 完毕。") );
}
定义可以读写的迭代器 it,然后循环遍历 m_salary 所有元素,获取每个元素的 value() ,注意读写迭代器返回的 it.value() 是一个可以读写的值引用,
it.value() *= 1.1;
 这句代码就是能够直接修改元素的 value 值,变为原来的 1.1 倍,就是涨工资 10% ,处理每个元素后,迭代器指向末尾的 m_salary.end(),结束循环。
因为 m_salary 内容发生变化,因此我们调用 UpdateSalaryShow() 更新列表控件,然后显示信息字符串到文本浏览框。

然后我们编辑“查找张三工资”按钮对应的槽函数:
//查找所有叫张三的人工资
void Widget::on_pushButtonFindZhang3_clicked()
{
    //张三有重名,找到第一个张三
    QMultiHash<QString, double>::const_iterator it;
    it = m_salary.find( tr("张三") );
    //信息字符串
    QString strInfo = tr("查找张三结果:\r\n");
    //查找多个张三
    while( it != m_salary.constEnd() )
    {
        if( it.key() == tr("张三") ) //连续的多个张三都显示
        {
            strInfo += it.key() + tr(" 工资: %1\r\n").arg( it.value() );
            ++it; //继续找下一个
        }
        else //人名不等于张三
        {
            break;  //遍历到 不是张三的位置,不需要再查找
        }
    }
    //显示
    ui->textBrowser->setText( strInfo );
}
定义一个只读迭代器 it,调用 m_salary.find() 找到第一个 "张三" 元素的迭代器位置,构造信息字符串 strInfo;
由于多哈希映射可能存在多个 "张三" 元素,因此我们通过循环找出多个 "张三" 元素:
如果 it.key() 是等于 tr("张三") ,那么为信息字符串增加一行,显示当前 "张三" 元素及其工资数额,迭代器向后移动;
直到遇到下一个元素的名字不等于 tr("张三") ,那么结束循环。
查询完成后,我们显示信息字符串到文本浏览框。
关键字相同时,键值对总是存储在临近的连续位置,因为计算的哈希表位置一样,find() 函数返回第一个匹配的元素迭代器位置。

下面我们编辑“查找工资最高3人”按钮对应的槽函数:
//查找工资前三的人
void Widget::on_pushButtonFindTop3_clicked()
{
    //检查员工人数,如果不超过三个人,就不处理
    if( m_salary.count() <= 3 )
    {
        ui->textBrowser->setText(tr("不超过三个人,不需要查询。"));
        return;
    }
    //遍历哈希对象,然后创建工资到人名的QMultiMap映射对象
    //哈希对象迭代器
    QMultiHash<QString, double>::const_iterator it;
    //工资到人名的反向映射
    QMultiMap<double, QString> mapOrder;
    //迭代处理
    for( it=m_salary.constBegin(); it!=m_salary.constEnd(); ++it)
    {
        mapOrder.insert( it.value(), it.key() );//反向映射
    }

    //QMultiMap  QMap 插入节点时,会自动按照 key 值排序,从小到大排序
    QString strInfo = tr("工资前三的员工:\r\n");
    QMultiMap<double, QString>::const_iterator itMap;
    itMap = mapOrder.constEnd(); //注意不能访问 end 虚假节点
    //查找最后三个即可
    for(int i=0; i<3; i++)
    {
        --itMap; //开头 --,会跳过 end 虚假节点
        strInfo += itMap.value() + tr(" 工资:%1\r\n").arg( itMap.key() );
    }
    //显示
    ui->textBrowser->setText( strInfo );
}
函数开头计算一下 m_salary 元素个数,如果不超过三个,那么没必要计算,直接显示信息字符串到文本浏览框,返回。
对应 4 个及以上的情况,执行后续操作。
我们定义只读迭代器 it,定义多映射类对象 mapOrder;
遍历 m_salary 对象每个元素,将工资作为 key,将人名作为 value,旧的 key-value 互换了,添加给 mapOrder,
这样 mapOrder 就会以工资数值为关键字来排序,元素的映射值为人名,与工资数额同步排序。
mapOrder 的元素添加完成后,排序也就自动完成了,按照工资从小到大排列,我们只需要读取排在最后的三个,就是工资最高的三个人。
后续我们就定义信息字符串 strInfo,然后定义只读迭代器 itMap,从迭代器末尾 constEnd() 开始迭代:
每轮循环开始时调用 --itMap ,这样就能跳过末尾的虚假节点,进入真实的最后元素,读取 key 工资和 value 人名,添加到信息字符串,
最后显示信息字符串到文本显示框。

接下来我们编辑“查找8K以上员工”按钮对应的槽函数:
//查找 8K 以上的员工
void Widget::on_pushButtonFind8K_clicked()
{
    //哈希对象迭代器
    QMultiHash<QString, double>::const_iterator it;
    //信息字符串
    QString strInfo = tr("查找8K以上工资的员工:\r\n");
    for( it=m_salary.cbegin(); it!=m_salary.cend(); ++it )
    {
        if( it.value() >= 8000 ) //判断 8K 以上的
        {
            strInfo += it.key() + tr(" 工资:%1\r\n").arg( it.value() );
        }
    }
    //显示
    ui->textBrowser->setText( strInfo );
}
我们定义只读迭代器 it ,定义信息字符串 strInfo;
然后循环遍历 m_salary 的每个元素,检查元素的 value() 是否达到 8000 以上,如果是 8000 以上,那么根据关键字和映射值构造字符串添加到 strInfo;
遍历结束后显示信息串到文本浏览框。

最后我们编辑“删除8K以上员工”按钮对应的槽函数:
//删除 8K 以上员工
void Widget::on_pushButtonDel8K_clicked()
{
    //读写迭代器
    QMultiHash<QString, double>::iterator it;
    //信息字符串
    QString strInfo = tr("删除8K以上工资的员工:\r\n");
    for( it=m_salary.begin(); it!=m_salary.end(); NULL )
    {
        if( it.value() >= 8000 ) //判断 8K 以上的
        {
            strInfo += it.key() + tr(" 工资:%1\r\n").arg( it.value() );
            //删除迭代器指向的元素,注意旧的 it 删除了不可用
            //必须用 erase() 返回值作为后面元素的新迭代器
            it = m_salary.erase( it );
        }
        else //不删除,直接下一个
        {
            ++it;
        }
    }
    //更新列表控件
    UpdateSalaryShow();
    //显示信息字符串
    ui->textBrowser->setText( strInfo );
}
我们定义读写的迭代器 it,定义信息字符串 strInfo;
然后从 m_salary.begin() 开始遍历,注意 for() 小括号里面第三段是 NULL,这里面不进行迭代器移动,因为删除元素操作特殊,不能简单在 for 小括号里移动迭代器;
我们判断超过 8000 工资的元素,构造字符串添加到 strInfo,然后调用 m_salary.erase( it ) 删除当前元素,并将 m_salary.erase( it ) 返回值作为新的迭代器存到 it,
因为删除元素后,旧的迭代器失效,不能再使用,必须用 erase() 返回的下一个元素迭代器赋值给 it,进行下一轮循环;
如果循环时 it 指向元素的工资不够 8000,那么我们才调用  ++it  移动到下一个元素进行下一轮循环。
迭代完成之后,我们调用 UpdateSalaryShow() 更新列表控件,并显示信息字符串到文本浏览框。
例子代码讲解到这,我们生成项目,运行例子,我们首先点击“查找工资最高3人”按钮,可以看到如下结果:
run1
查到了工资最高的三个人名单和工资金额,然后我们点击“删除8K以上员工”按钮,运行结果如下:
run2
在上半部分的列表框可以看到删除了 8000 以上工资的员工,信息显示到下半部分的文本浏览框了。其他按钮功能请读者自行测试。下面我们讲解 Java 风格迭代器内容。

9.5.2 Java 风格迭代器

除了STL 风格的迭代器,Qt 还为容器类定制了 Java 风格的迭代器,函数与 Java 语言的迭代器类相似。Java 语言通常不使用指针,所以不会用 *it 这种语法,都是用函数来进行元素访问。Java 风格的迭代器使用单独的类来封装,分为只读迭代器类和读写迭代器类,列举如下:

容器 只读迭代器 读写迭代器
QList<T>, QQueue<T>  QListIterator<T> QMutableListIterator<T>
QLinkedList<T>  QLinkedListIterator<T> QMutableLinkedListIterator<T>
QVector<T>, QStack<T>  QVectorIterator<T> QMutableVectorIterator<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>
QSet<T>  QSetIterator<T> QMutableSetIterator<T>

所有支持读写的迭代器类都带 Mutable 前缀,表示元素可变的迭代器。因为 Java 不使用指针,所以 Java 迭代器并不会直接指向元素,而像是指向元素前面或后面的狭缝,如下图所示:
java
Java 风格迭代器的示例代码如下:
QList<QString> list;
list << "A" << "B" << "C" << "D";

QListIterator<QString> i(list);
while (i.hasNext())
    qDebug() << i.next();
QListIterator<QString> i(list) 这句代码是根据列表对象定义一个 Java 风格迭代器,这时候迭代器 i 指向首元素前面的狭缝,
使用 i.next() 函数,会返回首元素,并且移动到迭代器的下一个狭缝。不存在 *i 或者 ++i 之类的用法, i.next() 函数把取值和移动位置两件事都办了。
判断结尾的方式是 i.hasNext() ,如果有下一个狭缝位置,说明没到结尾;否则没有下一个狭缝位置,循环结束。
循环结束时迭代器指向末尾元素后面的狭缝。

如果希望倒过来,从末尾开始迭代,那么使用下面代码:
QListIterator<QString> i(list);
i.toBack();
while (i.hasPrevious())
    qDebug() << i.previous();
i.toBack() 就是移动到末尾元素后面的狭缝,然后使用 i.previous() 函数,访问元素并向前移动;
循环移动直到开头元素前面的狭缝,开头元素前面的狭缝就没有更靠前的狭缝,循环结束。

正向遍历和反向遍历的过程是左右对称的,访问的元素总是  i.next() 函数或者 i.previous() 函数刚刚滑过的元素,如下图所示:
java2
对于只读迭代器,访问函数如下表所示:

函数 行为
void toFront() 移动到首元素前面的狭缝
void toBack() 移动到尾元素后面的狭缝
bool hasNext() const 如果位置不是尾元素后面的狭缝,返回 true
Item next() 向后移动一个狭缝位置,并返回狭缝前面的刚滑过的元素
Item peekNext() const 返回狭缝后面的元素,不移动狭缝位置
bool hasPrevious() const 如果不在首元素前面的狭缝位置,返回 true
Item previous() 向前移动一个狭缝位置,并返回狭缝后面的刚滑过的元素
Item peekPrevious() const 返回狭缝前面的元素,不移动狭缝。
bool findNext(const T & value) 从迭代器当前狭缝开始向后查找,如果找到匹配 value 的元素,移动到该元素后面的狭缝(正好滑过该元素),返回 true,否则移动到尾元素后面的狭缝,并返回 false
bool findPrevious(const T & value) 从迭代器当前狭缝开始向前查找,如果找到匹配 value 的元素,移动到该元素前面的狭缝(正好滑过该元素),返回 true,否则移动到首元素前面的狭缝,并返回 false
const Key & key() const
const T &  value() const
映射和哈希映射的迭代器函数,返回关键字和映射值

使用 Java 风格迭代器,一定要注意“刚滑过的元素”的概念,刚滑过的元素,就是迭代器当前读写的元素。
例如对于映射类对象的迭代器,示例代码如下:
    QMap<int, int> map;
    map.insert(1,1);
    map.insert(2,4);
    map.insert(3,9);
    //Java 风格迭代器
    QMapIterator<int, int> it( map );
    while( it.hasNext() )
    {
        it.next();
        qDebug()<<it.key()<<it.value();
    }

    //要从头开始 findNext(),如果从末尾开始 findNext()那么后面没有任何元素
    it.toFront();
    //查找
    bool bRet = it.findNext( 9 );
    if( bRet )
        qDebug()<<it.key()<<it.value();
    else
        qDebug()<<"Can not find.";
循环迭代时,it.next() 滑到后面一个狭缝,但是 key() 和 value() 访问的是刚滑过的元素。
对于 findNext() 函数,注意它是从迭代器当前狭缝开始往后查找元素 value,如果从末尾元素后面的狭缝开始 findNext() ,后面不存在任何元素,所以要在开始之前,调用 it.toFront() 移动到最开头的狭缝,这样才能遍历容器对象查找匹配的元素。
注意要检查 findNext() 返回值,如果为 true,那么可以使用 key() 和 value() 访问刚滑过的元素,就是找到的匹配元素,如果返回值为 false,那么迭代器指向末尾元素后面的狭缝,没有找到任何匹配内容。上面代码输出结果如下:
1 1
2 4
3 9
3 9
使用 Java 风格迭代器,也要注意不能出现越界访问,比如 toFront() 是首元素的前面狭缝,这时候不能调用 previous() 和 peekPrevious() 函数;
如果处在 toBack() 尾元素的后面狭缝,这时候不能调用 next() 和 peekNext() 函数;越界访问可能导致程序异常崩溃,需要特别注意判断越界条件。

对于支持读写的迭代器 QMutable***Iterator,会额外多出如下函数,用于修改 value 和删除元素:

函数 行为
void setValue(const T & value) 针对刚滑过的元素,设置 value 值
T & value() 返回刚滑过的元素的 value 的可读写引用,支持 it.value() += 1024 这种修改数值代码。
void remove() 删除刚滑过的元素

Java 风格迭代器的所有读写操作都是针对刚滑过的元素,而不管迭代器位置是在元素的前面或后面狭缝。例如下面代码:
    QMap<int, int> map;
    map.insert(1,1);
    map.insert(2,4);
    map.insert(3,9);
    //Java 风格读写迭代器
    QMutableMapIterator<int, int> it(map);
    while(it.hasNext())
    {
        it.next(); //滑过一个元素
        if( it.key() == it.value() )//对刚滑过的元素进行访问和处理
            it.remove(); //删除关键字等于映射值的元素
        else
            qDebug()<<it.key()<<it.value();
    }
上面代码能够准确删除关键字等于映射值的元素,然后打印其他未删除的元素内容,输出如下:
2 4
3 9

无论对于 STL 风格和 Java 风格迭代器,注意同一时间只能有一个读写迭代器处理容器对象,一个容器对象不能同时使用多个读写迭代器,那样会造成内存访问错乱,程序可能崩溃。如果使用只读迭代器,那么一个容器对象可以使用多个只读迭代器读取内容。

对于关联容器的迭代器可以写入的内容是 value ,关键字 key 是不能直接修改的,只能删除或重新添加。
对于 QSet 类,它的 value 其实相当于哈希映射的关键字,QSet 的元素在迭代器中只能读取和删除,而不能赋值修改。

Java 风格迭代器的内容介绍到这,下面我们学习一个模拟 TCP 连接管理的例子,例子仅使用模拟的 IP 地址和端口号作为数据存在容器里,并不真的进行网络连接。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 tcpmanager,创建路径 D:\QtProjects\ch09,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
打开 widget.ui 文件进入图形化编辑界面,构造界面如下图所示:
UI
界面第一行是一个“TCP连接列表”标签。第二行是一个树形控件 treeWidget。
第三行是“IP”标签、单行编辑器 lineEditIP、“端口”标签、旋钮编辑框 spinBoxPort,设置旋钮编辑框属性 sizePolicy 水平策略为 Expanding,第三行控件按照水平布局器排布。
第四行是三个按钮:“添加TCP连接”按钮 pushButtonAddTCP、“删除匹配IP连接”按钮 pushButtonDelIP、“删除匹配端口连接”按钮 pushButtonDelPort,这三个按钮按照水平布局器排布。
第五行是两个按钮:“查找1024以下小端口连接”按钮 pushButtonFindBelow1024、“给小端口号增加1024”按钮 pushButtonPlus1024,第五行按钮按照水平布局器排布。
最后一行是文本浏览框 textBrowser。窗口整体按照垂直布局器排列,窗口大小 440 * 560 。
界面编辑好之后,我们依次右击五个按钮,在右键菜单为每个按钮添加 clicked() 信号对应的槽函数。
下面我们编辑头文件 widget.h 的内容:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QMultiMap> //保存IP和端口映射

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

    //初始化填充
    void InitTCPLinks();
    //更新树形控件的显示
    void UpdateTreeShow();

private slots:
    void on_pushButtonAddTCP_clicked();

    void on_pushButtonDelIP_clicked();

    void on_pushButtonDelPort_clicked();

    void on_pushButtonFindBelow1024_clicked();

    void on_pushButtonPlus1024_clicked();

private:
    Ui::Widget *ui;
    //保存IP和端口信息
    QMultiMap<QString, int> m_tcplinks;

};

#endif // WIDGET_H
我们添加 QMultiMap 头文件包含,用于保存IP端口号信息。
窗口类添加多映射成员变量 m_tcplinks,关键字是 IP 地址字符串,映射值是端口号。
然后手动添加两个函数,InitTCPLinks() 用于初始化填充成员变量 m_tcplinks,UpdateTreeShow() 是将 m_tcplinks 内容显示到界面的树形控件,每次IP端口发生变化时,就调用该函数更新树形控件显示。其他代码都是自动生成的,包含五个按钮对应的槽函数。

接下来我们分块编辑 widget.cpp 源文件内容,首先是开头构造函数内容:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>
#include <QMapIterator>
#include <QMutableMapIterator>
#include <QTreeWidgetItem>
#include <QRegExp>
#include <QRegExpValidator>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //设置树形控件只有 1 
    ui->treeWidget->setColumnCount( 1 );
    ui->treeWidget->header()->setHidden( true ); //隐藏头部,未使用
    //设置 IP编辑框
    //定义 IPv4 正则表达式,注意 "\\" 就是一个反斜杠字符
    QRegExp re("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}"
               "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
    //新建正则表达式验证器
    QRegExpValidator *reVali = new QRegExpValidator(re);
    //设置给 lineEditIP
    ui->lineEditIP->setValidator(reVali);
    ui->lineEditIP->setText( tr("192.168.1.1") ); //默认值
    //设置端口范围
    ui->spinBoxPort->setRange( 0, 65535 );
    ui->spinBoxPort->setValue( 1500 ); //默认值

    //初始化填充连接信息
    InitTCPLinks();
    //更新树形控件的显示
    UpdateTreeShow();
}

Widget::~Widget()
{
    delete ui;
}

//初始化填充
void Widget::InitTCPLinks()
{
    m_tcplinks.clear(); //清空旧的
    m_tcplinks.insert( tr("192.168.1.1"), 80 );
    m_tcplinks.insert( tr("192.168.1.1"), 443 );
    m_tcplinks.insert( tr("192.168.1.2"), 20 );
    m_tcplinks.insert( tr("192.168.1.2"), 21 );
    m_tcplinks.insert( tr("192.168.1.3"), 80 );
    m_tcplinks.insert( tr("192.168.1.3"), 443 );
    m_tcplinks.insert( tr("192.168.1.3"), 3306 );
}
文件开头添加了多个类型头文件包含,然后在窗口的构造函数里,我们设置树形控件 ui->treeWidget 只有 1 列信息,并设置树头标题部分隐藏,因为这部分没使用;
然后定义正则表达式 re 和正则表达式验证器 reVali ,将验证器设置给单行编辑器 ui->lineEditIP,这样限定输入合法的 IPv4 地址;
设置单行编辑器初始地址字符串为 "192.168.1.1";
设置旋钮编辑器的数值范围 0 ~ 65535,就是端口号范围,然后设置初始端口号数值为 1500 ;
构造函数最后调用 InitTCPLinks() 填充初始的 IP端口号信息,并调用 UpdateTreeShow() 将IP端口映射对象的信息显示到树形控件。
InitTCPLinks() 函数内部比较简单,清空旧的内容,然后添加了 7 个连接元素给 m_tcplinks 对象。

下面我们来编辑 UpdateTreeShow() 函数内容,以 IP 地址为顶级树形节点,将所属端口号作为 IP 地址节点的子节点显示。
//更新树形控件的显示
void Widget::UpdateTreeShow()
{
    ui->treeWidget->clear(); //清空旧的
    //定义迭代器
    QMapIterator<QString, int> it( m_tcplinks );
    //保存迭代中旧的IP
    QString strOldIP;
    //保存旧的顶级IP节点
    QTreeWidgetItem *pOldTopItem = NULL;
    //端口号节点
    QTreeWidgetItem *pPortItem = NULL;
    //迭代遍历
    while( it.hasNext() )
    {
        it.next();  //滑过一个元素
        //获取滑过元素的 IP 和端口
        QString strIP = it.key();
        int nPort = it.value();
        //判断
        if( strIP != strOldIP )
        {
            //新的主机IP,建立顶级IP节点
            pOldTopItem = new QTreeWidgetItem();
            pOldTopItem->setText(0, strIP);
            ui->treeWidget->addTopLevelItem( pOldTopItem );
            //添加端口号为子节点
            pPortItem = new QTreeWidgetItem();
            pPortItem->setText( 0, tr("%1").arg(nPort) );
            pOldTopItem->addChild( pPortItem );
            //更新旧的IP
            strOldIP = strIP;
        }
        else
        {
            //现在元素IP  上一个元素IP一样
            //添加 pOldTopItem 子节点
            pPortItem = new QTreeWidgetItem();
            pPortItem->setText( 0, tr("%1").arg(nPort) );
            pOldTopItem->addChild( pPortItem );
        }
    }// end while
    //遍历结束,树形控件条目添加完成
    ui->treeWidget->expandAll(); //全部展开
}
该函数先清空旧的树形控件内容,然后定义只读迭代器 it,用于遍历 m_tcplinks 对象;
定义字符串 strOldIP 保存迭代过程中的旧 IP 字符串,只要 IP 字符串不变,那就将同主机 IP 的端口设置到一起作为子节点。
定义 pOldTopItem 保存同 IP 的顶级树形节点,定义 pPortItem 保存端口号子节点。
下面开始循环迭代,循环里面内容:
先调用 it.next() 滑过一个元素,刚滑过的元素就是我们要访问的元素;
获取刚滑过元素的 IP 和端口到 strIP、nPort 变量;
       比较当前 strIP 与旧的 strOldIP 是否相等,如果不相等,说明遇到新的主机 IP,那么我们新建树形条目,存到 pOldTopItem,设置文本为 strIP,并设置为树形控件新的顶级条目;然后为这个顶级条目新建一个子条目 pPortItem,子条目内容就是该主机 IP  的端口号;然后将新的主机 IP 地址存到 strOldIP,用于下轮循环比较;
       如果当前循环的 strIP 等于 strOldIP,那说明还是同一个主机 IP 的端口,那么直接新建一个条目 pPortItem ,内容是端口号,添加为顶级条目 pOldTopItem 的子条目。
循环结束后,设置树形控件展开所有子节点,方便显示所有的 IP 和端口号。

下面我们编辑 “添加TCP连接”按钮对应的槽函数:
//添加一个连接
void Widget::on_pushButtonAddTCP_clicked()
{
    //获取 IP 和端口
    QString strIP = ui->lineEditIP->text().trimmed();
    if( strIP.isEmpty() )
    {
        ui->textBrowser->setText( tr("IP为空。") );
        return; //IP为空
    }
    //端口
    int nPort = ui->spinBoxPort->value();
    //检查是否已存在相同的 IP和端口
    if( m_tcplinks.contains( strIP, nPort ) )
    {
        ui->textBrowser->setText( tr("该IP端口的连接已存在,IP和端口不能同时重复。") );
        return;
    }
    //不重复的连接,添加
    m_tcplinks.insert( strIP, nPort );
    ui->textBrowser->setText( tr("添加TCP连接完成。") );
    //更新树形控件
    UpdateTreeShow();
}
我们先获取 IP 地址字符串 strIP ,如果为空就不处理,如果非空,进行后续处理;
获取端口号存到 nPort ,判断容器对象 m_tcplinks 是否已经包含该 IP 和端口的连接,如果已存在了,那么不重复添加,返回;
如果没有包含该 IP  和端口,那么调用 m_tcplinks.insert( strIP, nPort ) 插入新元素,保存新的连接;
显示信息字符串,表示添加TCP连接完成,并更新树形控件。

接下来我们编辑“删除匹配IP连接”按钮对应的槽函数:
//删除匹配IP的连接
void Widget::on_pushButtonDelIP_clicked()
{
    //获取 IP
    QString strIP = ui->lineEditIP->text().trimmed();
    if( strIP.isEmpty() )
    {
        ui->textBrowser->setText( tr("IP为空。") );
        return; //IP为空
    }
    //删除计数
    int nDelCount = 0;
    //使用迭代器查找
    QMutableMapIterator<QString, int> it( m_tcplinks );
    //循环查找
    while ( it.hasNext() )
    {
        it.next(); //滑过一个元素
        if( it.key() == strIP ) //检查滑过元素的 key
        {
            //删除刚找到的滑过元素
            it.remove();
            nDelCount += 1; //更新删除计数
        }
    }
    //判断
    if( nDelCount < 1 )
    {
        ui->textBrowser->setText( tr("没有匹配的IP。") );
    }
    else
    {
        ui->textBrowser->setText( tr("已删除匹配IP的连接个数:%1 。").arg( nDelCount ) );
        //更新树形控件
        UpdateTreeShow();
    }
}
我们先获取 IP 地址字符串,如果非空才进行后面的操作;
定义删除计数 nDelCount,定义 m_tcplinks 对象的读写迭代器 it;
使用 while 循环遍历容器对象每个元素,在循环内部:
        先使用 it.next() 滑过一个元素,这个滑过的元素就是要访问的内容;
        我们判断滑过元素的 key() ,如果等于 strIP,那么找到匹配的元素,调用  it.remove() 删除刚滑过的元素,并让删除计数加一,进入下轮循环;
        如果 key() 不等于 strIP,不处理,直接进入下轮循环。
循环结束后,我们判断 nDelCount 删除计数,如果小于 1 ,说明没有匹配的IP,显示信息字符串,不需要更新树形控件;
如果 nDelCount  达到 1 以上,那么 显示信息串,删除了 nDelCount  个数的连接,并更新树形控件。

接下来我们编辑 “删除匹配端口连接”按钮对应的槽函数内容:
//删除匹配端口的连接
void Widget::on_pushButtonDelPort_clicked()
{
    //获取端口号
    const int nFindPort = ui->spinBoxPort->value();
    int nDelCount = 0; //删除计数
    //读写迭代器
    QMutableMapIterator<QString, int> it( m_tcplinks );
    //循环迭代
    it.toFront(); //从头开始
    //注意 Q*MapIterator 迭代器的 findNext()  findPrevious() 比较的是 value ;
    //  QMap/QMultiMap 容器类的 find() 比较的是 key 或者 key-value 对。
    while( it.findNext( nFindPort ) ) //端口号是 value,可以用迭代器的查找函数
    {
        it.remove();
        nDelCount += 1;
    }
    //遍历结束
    if( nDelCount < 1 )
    {
        ui->textBrowser->setText( tr("没有匹配的端口。") );
    }
    else
    {
        ui->textBrowser->setText( tr("已删除匹配端口的连接个数:%1 。").arg( nDelCount ) );
        //更新树形控件
        UpdateTreeShow();
    }
}
我们获取要比较的端口号 nFindPort ,定义删除计数 nDelCount;
定义 m_tcplinks 对象的读写迭代器 it,并移动到最前面;
然后开始循环迭代,循环判断的条件就是直接调用 it.findNext( nFindPort ) 查找该端口号,
如果找到该端口号,删除刚滑过的匹配元素,并让删除计数加一;
容器对象如果存在多个 nFindPort  端口,那么 while 循环会依次找出所有的匹配端口号元素并删除;
如果没找到,那么说明到了迭代器末尾,结束循环。
循环结束后,我们判断删除计数 nDelCount,如果小于 1,那么显示没有匹配的端口;
否则显示删除了 nDelCount 个数的连接,并更新树形控件显示。

接下来我们编辑“查找1024以下小端口连接”按钮对应的槽函数:
//找寻 <= 1024 的端口连接
void Widget::on_pushButtonFindBelow1024_clicked()
{
    QMapIterator<QString, int> it( m_tcplinks );
    QString strInfo = tr("1024以下端口号的连接:\r\n");
    //迭代查找
    while( it.hasNext() )
    {
        it.next();  //滑过一个元素
        if( it.value() <= 1024 )
        {
            strInfo += it.key() + tr(" 端口:%1 \r\n").arg(it.value() );
        }
    }
    //显示
    ui->textBrowser->setText( strInfo );
}
该函数先定义 m_tcplinks 容器对象的只读迭代器,然后定义信息字符串 strInfo;
循环遍历容器对象,使用 it.next() 滑过一个元素,然后判断刚滑过元素的 value() ,如果小于等于 1024,说明是要找的元素,根据 IP 和端口号构造字符串添加给 strInfo ;如果端口号大于 1024,跳过不处理,进入下轮循环。
循环结束后,显示信息字符串到文本浏览框。

最后我们编辑“给小端口号增加1024”按钮对应的槽函数:
//为所有的小端口号增加 1024
void Widget::on_pushButtonPlus1024_clicked()
{
    QMutableMapIterator<QString, int> it( m_tcplinks );
    QString strInfo = tr("修改旧的1024以下端口号的连接:\r\n");
    //迭代查找
    while( it.hasNext() )
    {
        it.next();  //滑过一个元素
        if( it.value() <= 1024 )
        {
            strInfo += it.key() + tr(" 端口:%1 \r\n").arg(it.value() );
            //修改端口号,增加1024
            it.setValue( it.value() + 1024 );
            //等同于  it.value() += 1024;
        }
    }
    //显示
    ui->textBrowser->setText( strInfo );
    //更新树形控件
    UpdateTreeShow();
}
该函数先定义 m_tcplinks 对象的读写迭代器,然后定义信息字符串 strInfo;
循环遍历容器对象,首先调用 it.next() 滑过一个元素,然后判断滑过元素的 value() 是否小于等于 1024,
如果满足条件,那么根据小端口号节点的IP和端口构造字符串添加给 strInfo ,然后调用 it.setValue() 修改滑过元素的映射值;
如果是大于 1024 的端口,那么不处理,直接进入下轮循环。
遍历结束后面,显示信息字符串到文本浏览框,并更新树形控件显示。
it.setValue( it.value() + 1024 );    这句代码,也可以替换为   it.value() += 1024;
这两句代码执行效果是一样的,读写迭代器  it.value() 可以作为左值,进行赋值修改元素的映射值。

例子代码讲解到这,我们生成运行示例,看到如下界面:
run1
可以看到每个端口号都显示为 IP 顶级节点的子节点,方便显示主机 IP 和端口的隶属关系。
然后我们设置端口号为 80,点击“删除匹配端口连接”按钮,查看效果:
run2
我们发现有两个端口号为 80 的连接被删除,说明正好删除了所有匹配端口号的连接。然后我们点击“给小端口号增加1024”按钮,看到端口号数值变化:
run3
程序将四个小端口号的数值,都加上了 1024,然后显示到了树形控件,大于 1024 的端口 3306 没有变化。其他按钮功能请读者自行测试,本节的内容介绍到这,我们下一章节开始新的知识学习,学习能够在界面上包裹多个子控件的控件容器。



prev
contents
next