本文转自:《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_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 迭代器遍历过程如下图所示:
每轮循环,迭代器都指向元素本身,除了 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 没有变化。其他按钮功能请读者自行测试,本节的内容介绍到这,我们下一章节开始新的知识学习,学习能够在界面上包裹多个子控件的控件容器。