[QT_036]Qt学习之数据容器的迭代器

本文转自:《Qt编程指南》        作者:奇先生

Qt编程指南,Qt新手教程,Qt Programming Guide

9.5 数据容器的迭代器


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

9.5.1 STL 风格迭代器


本章所有容器类型都有 STL 风格的迭代器,内嵌到类型内部,每个容器类都有只读迭代器和读写迭代器,如下表所示:

容器只读迭代器读写迭代器
QList<T>, QQueue<T> QList<T>::const_iteratorQList<T>::iterator
QLinkedList<T> QLinkedList<T>::const_iteratorQLinkedList<T>::iterator
QVector<T>, QStack<T> QVector<T>::const_iteratorQVector<T>::iterator
QMap<Key, T>, QMultiMap<Key, T> QMap<Key, T>::const_iteratorQMap<Key, T>::iterator
QHash<Key, T>, QMultiHash<Key, T> QHash<Key, T>::const_iteratorQHash<Key, T>::iterator
QSet<T> QSet<T>::const_iteratorQSet<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 迭代器遍历过程如下图所示:

每轮循环,迭代器都指向元素本身,除了 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 文件进入图形化编辑界面,构造界面如下图所示:

界面第一行是“人员工资列表”标签。第二行是一个列表控件 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人”按钮,可以看到如下结果:

查到了工资最高的三个人名单和工资金额,然后我们点击“删除8K以上员工”按钮,运行结果如下:

在上半部分的列表框可以看到删除了 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 风格迭代器的示例代码如下:

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() 函数刚刚滑过的元素,如下图所示:

对于只读迭代器,访问函数如下表所示:

函数行为
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 文件进入图形化编辑界面,构造界面如下图所示:

界面第一行是一个“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() 可以作为左值,进行赋值修改元素的映射值。

例子代码讲解到这,我们生成运行示例,看到如下界面:

可以看到每个端口号都显示为 IP 顶级节点的子节点,方便显示主机 IP 和端口的隶属关系。
然后我们设置端口号为 80,点击“删除匹配端口连接”按钮,查看效果:

我们发现有两个端口号为 80 的连接被删除,说明正好删除了所有匹配端口号的连接。然后我们点击“给小端口号增加1024”按钮,看到端口号数值变化:

程序将四个小端口号的数值,都加上了 1024,然后显示到了树形控件,大于 1024 的端口 3306 没有变化。其他按钮功能请读者自行测试,本节的内容介绍到这,我们下一章节开始新的知识学习,学习能够在界面上包裹多个子控件的控件容器。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值